typescript - 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]> }]
>;
解决方案
假设您有一个通用函数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
索引处的元素并将其映射到.T
T[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]}
,除非我错过了一些重要的东西。
推荐阅读
- react-native - 如何使用 React native 找到解决方案
- ios - ScrollToTtem 不流畅
- geolocation - 为什么通过 IP 地址打开 Prestashop 的地理定位在 PageSpeed Insight 上给我一个 503 错误?
- python - 读取文件和分隔符(python)
- firebase - 如何将 Firebase/Firestore 添加到 NRWL monorepo 内的现有 NestJS 应用程序中?
- r - 在第一个 NA 出现后删除列表列表中的所有列表位置 (R)
- django - Django ORM 传递模型方法值聚合
- c++ - PCL 1.8 正态估计。法线估计是否正确?
- csv - 通过将字段与 bash 中的另一个 csv 文件匹配来在 csv 文件中追加列
- sharetribe - Sharetribe 列表的子列表