由于您要在调用callPlugin()
(如callPlugin<Plugin>({⋯})
)时手动指定generic类型参数,因此需要在此处定义PluginParams<T>
,以便PluginParams<Plugin>
成为表单可接受输入的discriminated union:
type Demo = PluginParams<Plugin>
/* type Demo = {
action: "join";
data: {
groupId: string;
};
} | {
action: "addPoint";
data: {
point: number;
};
} */
有一种方法可以做到这一点:
type PluginParams<T extends {
actions: Record<keyof T["actions"], (p: any) => any>
}> = { [K in keyof T["actions"]]:
{ action: K, data: T["actions"][K] extends (p: infer P) => any ? P : never }
}[keyof T["actions"]]
我是constraining,T
,所以它有一个Record<keyof T["actions"], (p: any) => any>
类型的actions
属性,所以它只接受像Plugin
这样的东西.请注意,您可以使用string
来代替递归keyof T["actions"]
,但有时这并不能很好地处理接口类型(请参见Subtype of Record<string, X> without the index signature).
然后,我们所做的就是对T["actions"]
的属性进行map,并将它们转换为具有适当类型的action
和data
属性的对象,然后将其与键的并集进行index into,以得到并集.这种形式{[P in K]: F<P>}[K]
的索引映射类型使得K
(例如,K1 | K2 | K3
)中的并集在输出(例如,F<K1> | F<K2> | F<K3>
)中产生并集,被称为"分布式对象类型",并在microsoft/TypeScript#47F<K1> | F<K2> | F<K3>
中描述.
如果您将callPlugin
定义为接受PluginParams<T>
,则您可以验证PluginParams<Plugin>
是否成为所需的区分联合,以及您的呼叫是否按预期工作:
function callPlugin<T extends {
actions: Record<keyof T["actions"], (p: any) => any>
}>(v: PluginParams<T>) {
console.log(v);
}
callPlugin<Plugin>({
action: 'addPoint',
data: {
point: 100
}
})
callPlugin<Plugin>({
action: 'join',
data: {
point: 100 // error!
// Type '{ point: number; }' is not
// assignable to type '{ groupId: string; }'.
}
})
callPlugin<Plugin>({
action: 'join',
data: {
point: 100 // error!
// Type '{ point: number; }' is not
// assignable to type '{ groupId: string; }'.
}
})
请注意,这是一种编写和调用泛型函数的非标准方式.它要求调用方手动指定类型参数,这通常是推断出来的.我希望您的callPlugin()
函数也应该接受plugin
参数,从中可以推断T
,或者您应该重构,以便在任何人调用callPlugin()
之前省go 所有泛型内容,这样就没有人需要记住手动指定类型参数.从技术上讲,这超出了所问问题的范围,因此我不会深入探讨这一问题,只是要注意您的代码可以被调用者正确地使用.
Playground link to code个