首页 > 解决方案 > Typescript 4.0+ 是否能够使用映射的可变元组类型的函数?

问题描述

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

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

打字稿还不能为这个函数的结果推断出一个强类型:

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

首先,我定义了一个类型来描述映射到 observables 的数组:

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 并不高兴。函数的返回表达式突出显示错误“TS2322:类型'{ a:任何;}[]'不可分配给类型'MapToMyInterface'”

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

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

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

这可以在 Typescript 中使用吗?

在@jcalz 的回答之后编辑:为了后代,我想发布我的问题的原始动机,以及我能够从@jcalz 的回答中获得的解决方案。

我试图包装一个 RxJs 运算符,withLatestFrom来懒惰地评估传递给它的 observables(当你可能传递一个函数的结果时很有用,该函数在某处开始正在进行的订阅,就像store.select在 NgRx 中所做的那样)。

我能够像这样成功地断言返回值:

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]> }]
  >;

标签: typescripttypescript-typingstypescript4.0

解决方案


假设您有一个通用函数wrap(),它接受一个 type 的值T并返回一个 type 的值{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引用数组的第三个Date元素的事实,它是一个 Wrapped而不是 Wrapped string。我不得不等到运行时才能看到问题。


如果您查看标准 TypeScript 库的类型for Array.prototype.map(),您会明白为什么会发生这种情况:

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

当您调用things.map(wrap)时,编译器会推断出单个U类型,不幸的是,它将是{a: any},因为如果things是 type T extends any[],编译器所知道的所有元素things是它们可以分配给any

确实没有好的通用类型可以用来Array.prototype.map()处理callbackfn参数对不同类型的输入执行不同操作的情况。这将需要更高种类的类型,例如类型构造函数,TypeScript 目前不直接支持这些类型(有关相关功能请求,请参阅microsoft/TypeScript#1213 )。


但是,如果您有特定的泛型类型callbackfn,(例如, ),您可以使用映射的数组/元组类型(x: T) => {a: T}手动描述元组或数组上的特定类型转换。

这里是:

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]; }; }

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

请注意,由于标准库的类型map()不预期此特定的通用映射函数,因此您必须使用类型断言对其进行类型检查。如果您只关心编译器在没有类型断言的情况下无法验证这一点,那么这实际上是您在 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是它的索引。所以我会说只是返回{a: T[K]},除非我错过了一些重要的东西。


Playground 代码链接


推荐阅读