是否可以在Typescript中获得记录's值的数组类型?



我想获得记录值的所有类型。例如,如果我有一个这样的对象:

{
'a': 1,
'b': false
}

我想要一个包含numberboolean的类型。

我试图在不丢失类型的情况下实现某种函数映射:

type MyContext = { store: number[]; };
const obj: Record<string, (context: MyContext, ...args: any[]) => void> = {
log(ctx: MyContext, msg: string) {
console.log(msg);
},
add(ctx: MyContext, value: number) {
ctx.store.push(value);
}
};
const convert = <TContext>(context: TContext, objectWithFunctions: Record<string, (context: TContext, ...args: any[]) => void>) => {
//omit the first parameter and return a new object with modified functions
const result: Record<string, (...args: any[]) => void> = {};
Object.keys(objectWithFunctions).forEach((functionName) => {
const func = objectWithFunctions[functionName];
result[functionName] = (...args) => func(context, ...args);
});
return result;
};
const context = { store: [] };
const newObj = convert(context, obj);
newObj.log('some message');
newObj.add(12);
newObj.add();//should give an error
newObj.foo();//should give an error
console.log(newObj, context.store);

游乐场

这个实现是有效的,但它缺乏类型,因为any[]约束用于所有对象函数的第二个参数。这是一种以某种方式推断类型并返回一些更严格的类型而不是Record<string, (...args: any[]) => void>的方法吗?

首先,如果不丢弃初始化器中有关特定方法名称和参数的所有信息,就不能将obj注释为Record<string, (context: MyContext, ...args: any[]) => void>类型。如果你想让convert(context, obj)的输出类型知道logadd,那么你可能应该直接赋值给obj而不做任何注释;让编译器推断它的类型:

const obj = {
log(ctx: MyContext, msg: string) {
console.log(msg);
},
add(ctx: MyContext, value: number) {
ctx.store.push(value);
}
};

之后,如果编译器没有抱怨调用convert(context, obj),那么你就知道obj是合适的类型。


接下来,为了使convert()的输出具有强类型,它不仅在T(context的类型)中必须是泛型的,而且在objectWithFunctions中与方法名称/参数映射相关的类型参数中也必须是泛型的。我的方法是使新的类型参数A成为一个对象类型,它的键是方法名,它的值是参数列表,不包括T类型的初始context参数。

例如,对于obj,A将被指定为

{
log: [msg: string];
add: [value: number];
}

请注意,您实际上永远不会使用A类型的值,但它可以从objectWithFunctions输入推断出来,并且可以直接表示objectWithFunctions的类型和用A表示的返回类型。以下是输入:

const convert = <T, A extends Record<keyof A, any[]>>(
context: T,
objectWithFunctions: { [K in keyof A]: (context: T, ...rest: A[K]) => void }
) => {
const result = {} as { [K in keyof A]: (...args: A[K]) => void };
(Object.keys(objectWithFunctions) as Array<keyof A>)
.forEach(<K extends keyof A>(functionName: K) => {
const func = objectWithFunctions[functionName];
result[functionName] = (...args) => func(context, ...args);
});
return result;
};

因此,objectWithFunctions的类型是一个映射类型,其中A的每个属性K中的数组A[K]被转换为一个函数类型,参数类型为T,后接一个rest参数类型为A[K]。事实证明,由于该类型的形式为{[K in keyof A]...},因此它是同态映射类型,并且编译器擅长从同态映射类型进行推断。返回类型,如result的注释所示,是没有初始参数T的相同映射类型。

在函数体中,我必须使用一些类型断言来说服编译器某些值具有某些类型。result的初始值是一个空对象,所以我必须断言它将是最终类型(因为{}当然不是那种类型)。并且Object.keys()的返回类型只是string[](参见为什么不't Object)。),所以我必须断言它返回的是一个keyof A数组。


好的,让我们测试一下:

const context: MyContext = { store: [] }; 

在这里将context注释为MyContext是有用的,因为否则编译器会假设store将始终是一个空数组(类型为never[])。这里是:

const newObj = convert(context, obj);        

你可以通过智能感知使用快速信息来查看对convert()的调用导致编译器将MyContext推断为T,将前面提到的{ log: [msg: string]; add: [value: number];}类型推断为A:

/* const convert: <MyContext, {
log: [msg: string];
add: [value: number];
}>(context: MyContext, objectWithFunctions: {
log: (context: MyContext, msg: string) => void;
add: (context: MyContext, value: number) => void;
}) => {
...;
} */

当你把鼠标悬停在myObj上时,你可以看到它正是你想要的类型:

/* const newObj: {
log: (msg: string) => void;
add: (value: number) => void;
} */

所以它的行为符合预期:

newObj.log('some message');
newObj.add(12);
newObj.add(); // error
newObj.foo(); // error
console.log(newObj, context.store); // {}, [12]

Playground链接到代码

最新更新