Typescript 4.0+ 是否能够使用映射可变元组类型的函数?

Is Typescript 4.0+ capable of functions using mapped variatic tuple types?

举个例子,假设我有一个简单的函数,它将可变数量的事物映射到对象数组,例如 { a: value }.

const mapToMyInterface = (...things) => things.map((thing) => ({ a: thing}));

Typescript 还不能为该函数的结果推断强类型:

const mapToMyInterface = mapToInterface(1, 3, '2'); // inferred type{ a: any }[]

首先,我定义了一个描述映射到可观察对象的数组的类型:

type MapToMyInterface<T extends any[]> = {
  [K in keyof T]: T[K] extends Array<infer U> ? { a: U } : { a: T[K] }
}

现在我更新我的函数:

const mapToMyInterface = <T extends any[]>(...things: T): MapToMyInterface<T> => things.map((thing) => ({ a: thing}));

到目前为止,Typescript 并不开心。函数的 return 表达式突出显示错误“TS2322: Type '{ a: any; }[]' is not assignable to type 'MapToMyInterface'”

显然,参数thing需要在映射函数中显式键入。但我不知道怎么说“第 n 种”,这正是我需要的。

也就是说,既不将 thing 标记为 T[number],也不做以下工作:

const mapToMyInterface = <T extends any[], K = keyof T>(...things: T): MapToMyInterface<T> => things.map((thing: T[K]) => of(thing));

这可以在 Typescript 中使用吗?

在@jcalz 的回答后进行编辑: 为了 post 真实,我想 post 我的问题的最初动机,以及我能够从 @jcalz 的回答中得到的解决方案。

我试图包装一个 RxJs 运算符,withLatestFrom,以懒惰地评估传递给它的 observables(当你可能传递一个在某处开始持续订阅的函数的结果时很有用,比如store.select 在 NgRx 中执行。

我能够像这样成功断言 return 值:

export const lazyWithLatestFrom = <T extends Observable<unknown>[], V>(o: () => [...T]) =>
  concatMap(value => of(value).pipe(withLatestFrom(...o()))) as OperatorFunction<
    V,
    [V, ...{ [i in keyof T]: TypeOfObservable<T[i]> }]
  >;

您能否输入任意数量的不同类型的参数,并以相同的顺序返回这些类型的 Observables 数组? 否。您不能映射基于特定数字键的类型。

您能否输入那些相同的任意参数并返回一个 Observables 数组以及这些类型的联合? 是的。

type Unpack<T> = T extends (infer U)[] ? U : T;

const mapToObservable = <T extends any[]>(...things: T): Observable<Unpack<T>>[] => things.map((thing) => of(thing));

const mapped: Observable<string | number>[] = mapToObservable(1, 3, "2");

Playground Link

编辑: 我说得太早了,实际上 有可能以相同的顺序获得 return 类型,如 .

中所述

此 return 类型可为您提供所需的 return

type MapToMyInterface<T extends any[]> = any[] & {
  [K in keyof T]: {a: T[K]};
} & {length: T['length']};

但是从 array.map() 编辑的 return 的类型不能分配给它(由于 @jcalz 刚刚解释的原因),所以你必须断言你的 return使用 as.

类型是正确的
const mapToMyInterface = <T extends any[]>(...things: T): MapToMyInterface<T> => things.map((thing) => ({ a: thing})) as MapToMyInterface<T>;

const mapped = mapToMyInterface(1, 3, "2");
const [one, three, two] = mapped; // gets each element correctly

Playground Link

假设您有一个泛型函数 wrap(),它接受类型 T 的值,return 接受类型 {a: T} 的值,如您的示例所示:

function wrap<T>(x: T) {
    return ({ a: x });
}

如果您只是创建一个接受数组 things 并调用 things.map(wrap) 的函数,您将得到一个弱类型函数,正如您所注意到的:

const mapWrapWeaklyTyped = <T extends any[]>(...things: T) => things.map(wrap);
// const mapWrapWeaklyTyped: <T extends any[]>(...things: T) => {a: any}[]

这完全忘记了进入的各个类型及其顺序,而您只是一个 {a: any} 的数组。说的很对,但用处不大:

const weak = mapWrapWeaklyTyped(1, "two", new Date());
try {
    weak[2].a.toUpperCase(); // no error at compile time, but breaks at runtime
} catch (e) {
    console.log(e); // weak[2].prop.toUpperCase is not a function 
}

该死,编译器没有发现 2 指的是数组的 third 元素,它是一个包装的 Date 而不是一个包裹 string。我不得不等到运行时才能看到问题。


如果您查看 Array.prototype.map()standard TypeScript library's typing,您就会明白为什么会这样:

interface Array<T> {
  map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
}

当您调用 things.map(wrap) 时,编译器会推断出一个单一的 U 类型,不幸的是它将成为 {a: any},因为如果 things 是类型 T extends any[],编译器对 things 的元素的全部了解是它们可分配给 any.

确实没有什么好的 general 类型可以给 Array.prototype.map() 来处理 callbackfn 参数对不同类型的对象做不同事情的情况输入。这将需要更高种类的类型,例如 type constructors, which TypeScript doesn't currently support directly (see microsoft/TypeScript#1213 来满足相关功能请求)。


但是如果您的 callbackfn 具有特定的泛型类型(例如 (x: T) => {a: T} ),您可以使用 [= 手动描述元组或数组的特定类型转换51=].

这里是:

const mapWrapStronglyTyped = <T extends any[]>(...things: T) => things.map(wrap) as
    { [K in keyof T]: { a: T[K] } };
// const mapWrapStronglyTyped: 
//   <T extends any[]>(...things: T) => { [K in keyof T]: {a: T[K]; }; }

我们在这里所做的只是遍历 T 数组的每个(数字)索引 K,然后获取该索引处的 T[K] 元素并将其映射到{a: T[K] }.

请注意,由于 map() 的标准库类型没有预料到这个特定的通用映射函数,因此您必须使用 type assertion 来进行类型检查。如果您只担心编译器无法在没有类型断言的情况下验证这一点,那么这实际上是您在 TypeScript 中没有更高种类类型的情况下可以做的最好的事情。

您可以在与之前相同的示例上对其进行测试:

const strong = mapWrapStronglyTyped(1, "two", new Date());
try {
    strong[2].a.toUpperCase(); // compiler error, Property 'toUpperCase' does not exist on type 'Date'
} catch (e) {
    console.log(e); //  strong[2].prop.toUpperCase is not a function 
}
// oops, I meant to do this instead!
console.log(strong[1].a.toUpperCase()); // TWO

现在编译器发现了错误,并告诉我 Date 对象没有 toUpperCase 方法。万岁!


您的映射版本,

type MapToMyInterface<T extends any[]> = {
  [K in keyof T]: T[K] extends Array<infer U> ? { a: U } : { a: T[K] }
}

有点奇怪,因为你正在做映射两次;除非你传递数组的数组,否则没有理由检查 T[K] 它是否是一个数组本身。 T是数组,K是它的索引。所以我会说只是 return {a: T[K]} 除非我遗漏了一些重要的东西。


Playground link to code