打字稿方法 return 基于可选参数 属性 函数的类型

Typescript method return type based on optional param property function

尝试创建一个简单的实用程序:

type MapperFn<T, U> =  (val: T) => U;

interface mapperOpts<T,U> {
  cb?: MapperFn<T,U>
}

interface mapper {
  map<T, U, Z extends mapperOpts<T,U>>(arr: Array<T>, opts: Z): Z extends { cb: MapperFn<T,U> } ? U[]: T[];
}

const obj: mapper = {
  map: (arr, { cb }) => {
    if (!cb) return arr;
    return arr.map(cb);
  }
}

const arr: number[] =[1,2,3];

const result = obj.map(arr, {cb: (element) => element.toString() }); // should be typed as `string[]`

const result2 = obj.map(arr, { cb: (element) => element+1 }); // should be typed as `number[]`

const result3 = obj.map(arr, {}); // should be types as `number[]`

但是,我收到错误消息:

Type '<T, U, Z extends mapperOpts<T, U>>(arr: T[], { cb }: Z) => T[] | U[]' is not assignable to type '<T, U, Z extends mapperOpts<T, U>>(arr: T[], opts: Z) => Z extends { cb: MapperFn<T, U>; } ? U[] : T[]'.
  Type 'T[] | U[]' is not assignable to type 'Z extends { cb: MapperFn<T, U>; } ? U[] : T[]'.
    Type 'T[]' is not assignable to type 'Z extends { cb: MapperFn<T, U>; } ? U[] : T[]'.

请注意,resultresult2 标记为 unknown[],这可能意味着回调函数的参数类型推断无法正常工作。

我错过了什么?

Typescript Playground

具体如何 generic type argument inference works doesn't seem to be particularly well documented, aside from the now obsolete TypeScript Language Specification

但一般来说,当编译器看到对调用签名为 func<T, U, V>(x: F<T, U>, y: G<T, V>): H<U, V>; 且形式为 c = func(a, b) 的函数的调用时,它需要尝试推断类型参数 TUV 来自值 [=​​17=]、bc 的类型。要推断 U,编译器需要检查 ac 的类型,因为参数 x 和 return 类型都依赖于 U。因此 x 和 return 类型是 T 的潜在 推理站点 。另一方面,尝试使用 b 来推断有关 U 的任何内容是没有希望的,因为 y 参数的类型根本不引用 U .也就是说,y 不是 U.

的推理站点

并非所有推理站点都得到平等对待,有些推理站点比其他站点更难推理。 Return 类型通常是较差的推理站点,因为编译器通常不知道预期的 return 类型......如果你写 const c = func(a, b); 你是在要求编译器推断 c,所以 func() 的 return 类型未知。您只能在 return 类型的情况下使用,例如 c 已经是已知类型,例如 const c: SomeType = func(a, b);.

涉及类型参数的类型函数越复杂,推理站点的用处就越小。对于像 f<T>(x: T): void 这样的东西,对 f(a) 的调用很容易将 T 推断为 a 的类型。但是对于像 g<T>(x: T[keyof T]): void 这样的东西,几乎不可能从 g(a) 推断出 T。在前一种情况下,您是从相同类型的值推断 T 。简单。在后一种情况下,从该类型属性的联合值推断 T 。你甚至不知道如何开始。其他类型的函数往往介于这两个极端之间。如果您 运行 遇到问题,最好的办法是简化推理站点中的类型函数。

最后,有些地方根本不作为推理站点。像 h<T, U extends T>(u: U): void 这样的函数签名没有 T 的推理站点。 Generic constraints 在推断类型参数时不参考。也许您可能希望编译器从传递给 h() 的内容中推断出 U,然后从 U 中推断出 T,但事实并非如此。 T 肯定会无法推断,并且会退回到 unknown.

之类的东西

对于出现此问题的问题,您可以查看 microsoft/TypeScript#38183, microsoft/TypeScript#31529,可能还有许多其他问题(我会搜索“推理站点”)。


综上所述,我对您的 map() 方法的建议是:

interface Mapper {
  map<T, Z extends MapperOpts<T, any>>(arr: Array<T>, opts: Z):
    Z extends { cb: MapperFn<T, infer U> } ? U[] : T[];
}

之前的版本U没有合理的推理站点。相反,我们只会从 arropts 推断出 TZ。这很可能会成功。由此,我们可以使用 Z 通过显式 conditional type inference.

提取 U

让我们看看它是如何工作的:

const result = obj.map(arr, { cb: (element) => element.toString() }); // string[]
const result2 = obj.map(arr, { cb: (element) => element + 1 }); // number[]
const result3 = obj.map(arr, {}); // number[]

看起来不错!

Playground link to code