TypeScript 事件处理程序





0/5 (0投票)
TypeScript 事件处理程序。
引言
我关于 TypeScript 和 React 的介绍性演示引发了关于在 React 和 DOM 中键入事件处理程序的激烈讨论。 前者相当简单:React 中的事件行为或多或少与您期望的一致。 然而,TypeScript 对 DOM 事件的奇怪实现,引发了一场关于期望、权衡以及尝试键入像 JavaScript 这样快速而松散的动态语言的挑战的有趣讨论。
首先,React
React 事件处理程序接收合成的、React 特有的事件。 例如,onClick
处理程序可以期望传递相应的 React.MouseEvent
class MyComponent extends React.Component<P, S> {
// See: @types/react/index.d.ts
onClick(e: React.MouseEvent) {
// ...
}
render() {
return (
<button onClick={this.onClick}>
Click me!
</button>
);
}
}
如果我们检查相应的类型声明,我们会看到 React 为 onClick
等事件处理程序提供了一个非常具体的类型
// @types/react/index.d.ts
interface MouseEvent<T = Element> extends SyntheticEvent<T> {
// ...
}
type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>;
interface DOMAttributes<T> {
// ...
onClick?: MouseEventHandler<T>;
}
由于单击需要鼠标,因此期望 onClick
接收 MouseEvent
(而不是 KeyboardEvent
)是有道理的。 用其他任何东西调用 onClick
将正确地产生编译器错误,并且 - 如果我们只关心 React - 那就是全部。
DOM 呢?
将 React 视为构建在文档对象模型 (DOM) 之上的“一次编写,随处运行”层。 开发人员不必处理古怪的浏览器 API,而是可以使用一致的、与供应商无关的世界视图。 而且正如任何以另一种方式完成它的人都知道的那样,这是一件非常了不起的事情。
不过,有时我们必须穿透 React 的友好的抽象,并处理底层的东西。 在这里,事件处理变得更有趣。 设置非常简单
const onClick = (e: MouseEvent) =>
console.log(`(${e.clientX}, ${e.clientY})`);
window.addEventListener('click', onClick);
但是这里有一个令人惊讶的实现值得额外关注。
题外话:为什么要进行类型检查?
对 TypeScript(或任何其他静态类型检查器)的投资会带来保证,即我们程序中的数据格式良好。 任何关于这些保证的问题都会引发关于我们投资价值的进一步、令人不安的问题。 我们期望 TypeScript 告诉我们何时出现问题。
我敢打赌你知道接下来会发生什么。
邪恶的东西
让我们做一个小实验。 除了单击鼠标之外,我们现在还允许用户使用按键“单击”。
const onClick = (e: MouseEvent) =>
console.log(`(${e.clientX}, ${e.clientY})`);
window.addEventListener('click', onClick);
window.addEventListener('keydown', onClick);
正如人们所期望的那样,keydown
将产生一个 KeyboardEvent
——而不是回调所期望的 MouseEvent
。 然而,此更改会编译! 它也可以运行,尽管控制台输出不会特别有用
> (undefined, undefined)
编译器无法在编译时知道将 KeyboardEvent
传递给 onClick
是否安全——但由于某种原因,它仍然允许这种行为。
在我的类型系统中?
类型系统用可靠性(一个可靠的类型系统会捕获所有错误,无论它们在运行时是否可能发生)和完整性(一个完整的系统只会报告可能的错误)来描述。 理想的系统是可靠和完整的。
TypeScript 的贡献者都是聪明人,他们无疑非常清楚这一点。 然而,我们有确凿的证据表明 TypeScript 的行为不健全。 这也不是唯一的案例:编译器会忽略许多特殊情况。
这些情况是经过深思熟虑的。 TypeScript 的设计目标之一是保持 JavaScript 的超集。 这意味着允许 JavaScript 允许的大部分行为,从广义上讲,就是很多。
其中之一是回调重用:一种不寻常但并非不常见的模式,TypeScript 特意迁就(顺便说一句,flowtype 不会)。 想要将鼠标事件和键盘事件绑定到同一个处理程序? 这是你的绳子。
本着可扩展 Web 的精神,React 可以制定自己的规则。 其中一条规则是一致的回调行为。
结论
即使 TypeScript 遵守 JavaScript 规则,编译器也允许的行为也有一定的限制。 让我们尝试调整 onClick
以期望一个数字
const onClick = (n: number) => { /* ... */ }
window.addEventListener('click', onClick);
这一次,我们会看到预期的编译器错误
// Argument of type '(e: number) => void' is not assignable to
// parameter of type 'EventListenerOrEventListenerObject'.
// Type '(e: number) => void' is not assignable to type 'EventListenerObject'.
// Property 'handleEvent' is missing in type '(e: number) => void'.
一个 KeyboardEvent
应该是一个 MouseEvent
,无论如何。 但即使适应浏览器 API,一个 number
也太过分了。 即使是不健全的案例也有其局限性。