是否可以根据未传入的参数 属性 有条件地缩小 return 类型?

Is it possible to conditionally narrow a return type based on a parameter property that's not passed in?

我认为我的代码比标题更能说明我想问的问题:

export const objs = [
  {    
    name: '1',
    x: 5,
   discriminator: true
  },
  {    
    name: '2',
   discriminator: false
  },
] as const;

export type ObjName = typeof objs[number]['name'];

export type Obj = HasX | NoX;

export interface HasX {
  name: ObjName;
  x: number;
  discriminator: true;
}

export interface NoX {
  name: ObjName;  
  discriminator: false;  
}

const typeCheckObjs = (obj: readonly Obj[]): void => {};
typeCheckObjs(objs);

// I want this return type to be conditional instead of a union. Conditional on the objName's discriminator value
const howDoIDoThis = (objName: ObjName): HasX | NoX => {
  return objs.find((obj) => {return obj.name === objName})!
}

You can experiment with this code in the Playground

我想知道是否可以有条件地 return 最后一个函数中的类型?为此,我需要将 objName 关联回列表中的元素并找出它是什么类型,然后有条件地 return 该类型。

我认为这不能完成有几个原因:

  1. 根据编译器,该列表中的元素实际上不是这些类型。
  2. 需要复杂的逻辑才能将 object 名称关联回数组中的元素。

如果无法实现我的要求,我可以在某些方面调整代码。但我仍然希望能够让编译器检查 objName: ObjName 的正确性。


这是我试过的方法:

const howDoIDoThis = <T extends Obj,>(objName: T["name"]): T extends HasX ? HasX : NoX => {
  const found = objs.find((obj) => {return obj.name === objName})!
  return found.discriminator ? found as HasX : found as NoX;
}

根据编译器,类型签名没问题,但最后一行无法编译。

无论如何,我不完全相信这个类型签名是我想要的。

这是另一个错误消息相同的尝试:

const howDoIDoThis = <T extends Obj,>(objName: T["name"]): T["discriminator"] extends true ? HasX : NoX => {
  const found: Obj = objs.find((obj) => {return obj.name === objName})!
  return found.discriminator === true ? found as HasX : found as NoX;
}

这可以编译,但错误表明它没有提供任何有用的好处:

const howDoIDoThis = <T extends Obj,>(objName: T["name"]): T["discriminator"] extends true ? HasX : NoX => {
  const found: Obj = objs.find((obj) => {return obj.name === objName})!
  return (found.discriminator ? found as HasX : found as NoX) as T["discriminator"] extends true ? HasX : NoX
}

如果名称是唯一的并且objs 是只读对象的只读数组,您可以通过映射数组类型来静态确定类型。使用几个实用程序类型和一个映射类型 FilterByName,您可以像这样定义一个类型 FindByName

type IndexKeys<A> = Exclude<keyof A, keyof []>
type ArrToObj<A> = {[K in IndexKeys<A>]: A[K]}
type Values<T> = T[keyof T]

type FilterByName<O extends Record<any, {name: string}>, N> = {
  [K in keyof O]: O[K]['name'] extends N ? O[K] : never
}

type FindByName<N> = Values<FilterByName<ArrToObj<typeof objs>, N>> 

我们的想法是,我们使用ArrToObj删除数组属性,得到一个以字符串索引('0''1'、..)作为键和数组元素的对象作为价值观。使用 FilterByName 然后我们将名称错误的值设置为 never,并使用 Values 提取过滤后的对象类型。

我们现在可以通过将签名 obj is FindByName<N> 添加到查找回调来键入 howDoIDoThis

const howDoIDoThis = <N extends ObjName>(objName: N) =>
  objs.find((obj): obj is FindByName<N> => {return obj.name === objName})!

对现有名称的 howDoIDoThis 应用程序将获得正确的类型:

const t1 = howDoIDoThis('1') // t1: { readonly name: "1"; readonly x: 5; readonly discriminator: true }
const t2 = howDoIDoThis('2') // t2: { readonly name: "2"; readonly discriminator: false }
const t3 = t1.x // t3: 5
const t4 = t2.x // type error: "Property 'x' does not exist on type .."

TypeScript playground

为什么条件 return 类型不起作用

当您尝试 return 条件类型时,您只是遇到了 TypeScript 的设计限制 - 不幸的是,无法 return 将根据类型解析的条件类型范围。请参阅回购协议中的 this issue 进行讨论。

解决方法

一个可能的解决方案是将条件 return 类型作为类型参数显式传递给 find 方法,但有一个问题。问题(不确定是否可以这样做)在于 ReadonlyArray 接口的定义方式。

它接受一个泛型参数作为它包含的值的类型,所以在定义 find 方法时,它的类型参数 S 必须是 [=18= 的子类型]:

find<S extends T>(predicate: (this: void, value: T, index: number, obj: readonly T[]) => value is S, thisArg?: any): S | undefined;

由于您的接口比相应类型的元组成员更宽,因此它们的并集不能分配给 T 因为 number(接口中的 x 属性)不可分配给 5(成员类型中的 x)。

这可以通过利用声明合并并向 ReadonlyArray 接口添加重载来解决,如下所示:

interface ReadonlyArray<T> {
  find<U>(predicate: (value: T, index: number, obj: readonly T[]) => unknown, thisArg?: any): { [ P in keyof this ] : this[P] extends U ? this[P] : never }[number] | undefined;
}

该技术依赖于将 this 推断为一个元组,然后您可以过滤该元组以获取已解析条件类型的可分配性并提取剩余的值。之后,您可以调整 howDoIDoThis 签名以将条件类型传递给 find 方法,瞧:

const howDoIDoThis = <T extends ObjName>(objName: T) => objs.find< T extends "1" ? HasX : NoX >((obj) => obj.name === objName)!;

howDoIDoThis("1"); //{ readonly name: "1"; readonly x: 5; readonly discriminator: true; }
howDoIDoThis("2"); //{ readonly name: "2"; readonly discriminator: false; }

当然,当您未指定类型参数时,这会使 find 看起来有点奇怪,因为现在正在推断 unknown(此签名在类型参数上更为宽松,优先超过 S extends T 个),但它不会在推理中丢失:

const normalTuple = [1,2,3] as const;
const neverMatches = normalTuple.find<string>((b) => b > 3); //undefined;
const hasMatch = normalTuple.find<number>((b) => b > 3); //1 | 2 | 3 | undefined;

唯一需要注意的是,正如您从上面看到的那样,现在您可以传递与元组中的值类型无关的任意类型,但是 return 类型将被过滤为 never | undefined -> undefined 将被类型保护捕获。

Playground