65.9K
CodeProject 正在变化。 阅读更多。
Home

TypeScript 事件处理程序

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2019年2月13日

MIT

3分钟阅读

viewsIcon

6886

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 也太过分了。 即使是不健全的案例也有其局限性。

© . All rights reserved.