推断的参数类型意外

时间:2019-03-29 03:21:22

标签: typescript

在下面的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,它就可以工作。我在做什么错了?

2 个答案:

答案 0 :(得分:4)

基本上,这里的核心问题是TypeScript跟踪类型,而不是值,并且除非您纯粹考虑为type提供的具体值,否则您的函数是无法证明正确的。

考虑一下您是否编写了以下代码:

const ev = new Eventer();
const rando = Math.random() > 0.5 ? "splat" : "empty";
ev.emit(rando);

这是一个合法的调用,它会破坏您的实现,因为K"splat" | "empty",并且该字符串并集通过事件映射映射到参数类型{{1} },并且提供的空白参数确实满足其中之一。

另请参阅https://github.com/Microsoft/TypeScript/issues/30581

答案 1 :(得分:3)

  

当您检查回调的类型时,它会报告const callback(arg0:number&Point,arg1:number)=> void,这似乎是完全错误的。

这是TypeScript中通用方法参数的工作方式。编译器只有emit方法的一个实例,该实例必须对K的任何可能值起作用,它不会为K的每个可能值创建单独的实例。具有类型检查的单个实例的唯一方法是拥有一个满足EventMap的所有可能类型的回调,这就是为什么参数类型为arg0: number & Point, arg1: number的原因。例如,第一个是numberPoint的交集,这是不可能的类型,因为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