我想获得记录值的所有类型。例如,如果我有一个这样的对象:
{
'a': 1,
'b': false
}
我想要一个包含number
和boolean
的类型。
我试图在不丢失类型的情况下实现某种函数映射:
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)
的输出类型知道log
和add
,那么你可能应该直接赋值给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链接到代码