在下面的TypeScript示例中,编译器抱怨callback(...args);
。它说Expected 2 arguments, but got 0 or more.
很奇怪。当您检查callback
的类型时,它会报告const callback(arg0: number & Point, arg1: number) => void
,这似乎是完全错误的。
interface Point {
x: number;
y: number;
}
interface EventMap {
'point': (element: Point) => void;
'empty': () => void;
'splat': (x: number, y: number) => void;
}
class Eventer {
private storage: EventMap;
constructor() {
this.storage = {
point: () => { },
empty: () => { },
splat: () => { },
};
}
public on<K extends keyof EventMap>(type: K, callback: EventMap[K]): void {
this.storage[type] = callback;
}
public emit<K extends keyof EventMap>(type: K, ...args: Parameters<EventMap[K]>): void {
const callback = this.storage[type];
callback(...args);
}
}
const ev = new Eventer();
ev.on('point', (point) => {
console.log(point.x);
});
ev.on('empty', () => {
console.log('nothing');
});
ev.on('splat', (x, y) => {
console.log(`Got ${x} and ${y}`);
});
ev.emit('point', { x: 100, y: 200 });
ev.emit('empty');
ev.emit('splat', 300, 400);
我已将其复制到TypeScript Playground中。同样奇怪的是,如果您实际上尝试运行已转译的JavaScript,它就可以工作。我在做什么错了?
答案 0 :(得分:4)
基本上,这里的核心问题是TypeScript跟踪类型,而不是值,并且除非您纯粹考虑为type
提供的具体值,否则您的函数是无法证明正确的。
考虑一下您是否编写了以下代码:
const ev = new Eventer();
const rando = Math.random() > 0.5 ? "splat" : "empty";
ev.emit(rando);
这是一个合法的调用,它会破坏您的实现,因为K
是"splat" | "empty"
,并且该字符串并集通过事件映射映射到参数类型{{1} },并且提供的空白参数确实满足其中之一。
答案 1 :(得分:3)
当您检查回调的类型时,它会报告const callback(arg0:number&Point,arg1:number)=> void,这似乎是完全错误的。
这是TypeScript中通用方法参数的工作方式。编译器只有emit
方法的一个实例,该实例必须对K
的任何可能值起作用,它不会为K
的每个可能值创建单独的实例。具有类型检查的单个实例的唯一方法是拥有一个满足EventMap
的所有可能类型的回调,这就是为什么参数类型为arg0: number & Point, arg1: number
的原因。例如,第一个是number
和Point
的交集,这是不可能的类型,因为number和Point没有共同点,并且它们的交集减少为never
,但从形式上讲是满足所有3个变量('point'
,'empty'
和'splat'
的签名的唯一方法(请注意,带有参数的函数与不带参数的函数兼容-因为如果一个函数只忽略它就可以了其参数)。
这就是expected 2 arguments
部分的来源。
对got 0 or more
的解释又来自于这样一个事实,即K
可以是扩展keyof EventMap
的任何内容,包括最允许的keyof EventMap
类型本身。因此,当callback(...args)
为args
时,Parameters<EventMap[keyof EventMap]>
的类型必须正确,这是3个可能参数的联合类型:[Point] | [] | [number, number]
。当参数是联合类型时,这意味着实际值可以是三个值中的任何一个,因此,当args例如callback(...args)
(联合的第二个成员)时,[]
的类型必须正确。失败,并且当类型检查失败时,编译器在此过程中看到的所有失败检查中仅显示一条错误消息。
TL; DR没有类型声明就无法对这类代码进行类型检查:
public emit<K extends keyof EventMap>(type: K, ...args: Parameters<EventMap[K]>): void {
const callback = this.storage[type] as (...args: Parameters<EventMap[K]>) => void;
callback(...args);
}
这很好,只要您在调用args
时传递始终与type
相对应的正确emit
。