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");
编辑: 我说得太早了,实际上 有可能以相同的顺序获得 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
假设您有一个泛型函数 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]}
除非我遗漏了一些重要的东西。
举个例子,假设我有一个简单的函数,它将可变数量的事物映射到对象数组,例如 { 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");
编辑: 我说得太早了,实际上 有可能以相同的顺序获得 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
假设您有一个泛型函数 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]}
除非我遗漏了一些重要的东西。