起因是在初始化mitt
实例时,发现如果传入interface作为泛型时会抛出异常:
类型"Events"不满足约束"Record<EventType, unknown>"。
类型"Events"中缺少类型"string"的索引签名。ts(2344)。
于是去看了一下传入的泛型Event
的定义:
typescript
export declare type EventType = string | symbol;
// ...
export default function mitt<Events extends Record<EventType, unknown>>(all?: EventHandlerMap<Events>): Emitter<Events>;
这里mitt对传入的泛型Events用extends
关键字做了个约束,要求Events必须满足Record<EventType, unknown>
,也就是说Events必须是一个键为字符串字面量,值为unknown
的Record
类型。
其中type
有隐式索引签名(implicit index signature)Record<PropertyKey, unknown>
自动满足条件,但是interface
是一种静态的类型声明,它并没有隐式索引签名,因此会抛出异常,如果需要使用interface
,需要手动添加索引签名:
typescript
interface Events {
// ...
[key: string | symbol]: unknown
}
但是如果这样写的话,会出现以下问题:
- 接口就的类型约束就被破坏了,key可以是任意的字符串,无法确保事件的key是上面编写的字符串字面量;
- 接口可以被任意扩展,可能添加非法事件;
- 无法利用类型系统进行静态检查。
typescript
interface Events {
// ...
}
interface Events {
[key: number]: string // 新增了数字key
}
为什么mitt要用Record<EventType, unknown>
来做限制而不是Record<EventType, any>
:
unknown
和 any
虽然都代表"不确定的类型",但它们在类型安全性上有着本质区别:
-
unknown
是类型安全的顶级类型- 它是 TypeScript 3.0 引入的类型安全的"任意值"表示
- 任何值都可以赋给
unknown
类型变量 - 但在使用前必须进行类型检查或类型断言
-
any
是类型系统的逃生舱- 它会完全绕过 TypeScript 的类型检查
- 可以对其执行任何操作而不触发类型错误
- 相当于回到了纯 JavaScript 的开发模式
参考资料: