让我们直接进入示例。
在下面的代码中,您可以看到使用了 keyof
,但 typescript 仍然将 EVT
的类型计算为 string|number|string
,即使 EVT
扩展 keyof ((typeof Events)[OBJ])
代码:(最后一行自动完成和类型检查工作正常)
export const Events = {
// [event: string]: (...args: any) => void;
account: {
available: (status?: 'online'|'offline',foo?:'test',bar?:'test2') => {
console.error('1');
},
}
}
export class AMQP {
static event<
OBJ extends keyof typeof Events,
EVT extends keyof ((typeof Events)[OBJ]),
FUNC extends ((typeof Events)[OBJ][EVT])
>(
obj: OBJ,
event: EVT,
...args: Parameters<FUNC>
) {
}
}
AMQP.event('account','available','online','test','test2');
问题(悬停在“FUN”上的红旗)
Type 'FUNC' does not satisfy the constraint '(...args: any) => any'.
Type '{ account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][EVT]' is not assignable to type '(...args: any) => any'.
Type '{ account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][keyof { account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ]]' is not assignable to type '(...args: any) => any'.
Type '{ account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][string] | { account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][number] | { ...; }[OBJ][symbol]' is not assignable to type '(...args: any) => any'.
Type '{ account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][string]' is not assignable to type '(...args: any) => any'.(2344)
更新:::
截至目前。按照 @Maciej Sikora 的建议,通过强制将 FUN 声明为函数修复错误来解决问题。
但这是另一个例子。无需强制声明即可正常工作的地方 唯一的区别是事件不深,所以它可以在那里工作,但当对象更深一点时就不行了,我仍然很想知道原因。
最佳答案
按条件类型缩小类型
我们可以通过明确地告诉 TS 我们只想要函数来解决这个问题,尽管目前只有看起来 TS 无法缩小它的范围。考虑:
type E = typeof Events // for readability
export class AMQP {
static event<
OBJ extends keyof E,
EVT extends keyof E[OBJ],
FUNC extends E[OBJ][EVT] extends (...a:any[]) => any ? E[OBJ][EVT] : never
>(
obj: OBJ,
event: EVT,
...args: Parameters<FUNC>
) {
}
}
要点是 - FUNC extends E[OBJ][EVT] extends (...a:any[]) => any ? E[OBJ][EVT]:从不
。这意味着我们只允许在对象结构的第二层中选择函数。看来现在TS不再提示了。
这种解决方案的额外优点是,如果我们提供一个不是函数的字段,则无法使用该选择调用该函数。
为什么TS无法缩小类型
有一些原因可以部分解释它。考虑类似的例子:
const a = {
a: 2
}
const f = <A extends typeof a, K extends keyof A>(a: A, k: K) => {
return a[k] + 1; // error we cannot use +
}
f(a, 'a');
如果a
里面只有数字值,我们就不能使用+
,这是怎么回事。看起来好像是错误的?不,它是正确的,因为我们的函数表示我们允许扩展 typeof a 的类型,因此它与 a
不同,我们可以拥有具有属性 a 的对象
还有另一个属性b
,例如 bool 值。考虑下面的代码可以编译:
const b = {
a: 2,
b: true,
}
f(b, 'b');
所以我可以使用结构上具有相同字段的对象 - a
但也有另一个具有不同类型的对象。出于同样的原因,您的代码无法缩小类型,因为我们可以引入另一个结构相同的对象,但具有另一个非函数属性。
在我们的示例中,我们的情况略有不同,因为我们引用的是一个单一对象。那么,尚不完全清楚为什么在某些嵌套级别上推理会失败。 TS 需要额外的帮助来缩小功能范围。为什么对我来说是未知的,看起来像是推理限制。
为事件引入静态类型
您应该考虑的第二个解决方案是静态类型化事件接口(interface)。考虑:
type Events = Record<string, Record<string, (...a: any[]) => any>>
export const events = {
account: {
available: (status?: 'online' | 'offline', foo: 'test', bar: 'test2') => {
console.error('1');
},
}
};
export class AMQP {
static event<
INSTANCE extends Events,
OBJ extends keyof INSTANCE,
EVT extends keyof INSTANCE[OBJ],
FUNC extends INSTANCE[OBJ][EVT]
>(
inst: INSTANCE,
obj: OBJ,
event: EVT,
...args: Parameters<FUNC>
) {
}
}
// pay attention below I pass events object explicitly as first argument
AMQP.event(events, 'account', 'available', 'online', 'test', 'test2' );
请注意,我在这里介绍了一些内容:
- 事件类型
- 额外的第一个参数是Events接口(interface)的实例对象
这种解决方案更加灵活,因为实现不绑定(bind)到一个对象,您可以通过 Events
接口(interface)传递任何结构正确的对象,也不需要条件类型。
关于Typescript 泛型,正在使用的关键,但它也使用符号 | 进行计算数量 |字符串也是如此,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59765734/