【译】结合使用 TypeScript 与 React

5,059 阅读8分钟

原文:Using TypeScript with React
作者:Simon Knott
译者:不肥的肥羊
为保证文章的可读性,本文采用意译
阅读全文约 13 分钟

这篇文章是基于我过去在 React JS & React Native Bonn Meetup 上所做的演讲。文章的目的是回答以下几个问题:

  • 什么是 TypeScript?
  • 如何在 React 中使用 TypeScript?
  • 为什么我们(不)应该使用 TypeScript?

这是我在学校之外的第一次公开演讲,能够顺利结束真是极好的,谢谢所有前来捧场的朋友!😁演讲是我非常喜欢的一件事情,希望未来能做更多的演讲。

什么是 TypeScript?

typescriptlang.org 对 TypeScript(缩写为 TS)有如下解释:

TypeScript 是 JavaScript 的类型超集,可以编译为纯 JavaScript。

上面这段描述可以解释为:

  1. TypeScript 是一种与 JavaScript 以某种形式关联的类型语言。它们主要的区别就在于,TypeScript 是一种静态类型语言,而 JavaScript 则是动态类型的。
  2. TypeScript 可以被编译为纯 JavaScript,然后在诸如浏览器或 Node.js 等环境中执行。
  3. 作为 JavaScript 的超集,意味着 TypeScript 只会在 JavaScript 的基础上添加新特性,同时仍与底层 JS 规范保持严格的兼容性。

让我们看一个栗子,由于 TS 是 JS 的超集,因此以下的代码对两种语言都是有效的:

function add(a, b) {
  return a + b;
}

const favNumber = add(31, 11);

我们已经了解到,TS 的特殊性在于它的类型系统。那么我们为以上的代码添加类型声明,就会产生下面符合 TS 规范,却不符合 JS 规范的代码:

function add(a: number, b: number): number {
  return a + b;
}

const favNumber: number = add(31, 11);

通过添加这些注解,TypeScript 编译器可以检查你的代码以查找出一些代码层面的 bug。例如,它可能会发现不匹配的参数类型,或者作用域外的调用等等。

使用 tsc 命令行工具编译 TypeScript 会经历:对代码进行类型检查 -> 所有检查都通过 -> 输出纯 JavaScript。输出的代码与源代码很相似,只是没有类型注解。

TypeScript 还支持基本类型推断,它通过省略函数返回值的类型以及大多数情况下的变量赋值类型来简化代码:

function add(a: number, b: number) {
  return a + b;
}

const favNumber = add(31, 11);

(与上一段代码的区别是,函数返回值没有类型注解。)

TS 中有效的基本类型有 booleanstringnumberSymbolnullundefinedBigInt 类型;还有 void 类型,用于表示函数不返回任何内容;还有 Function 类型,以及,通常用 Array<T> 来表示 string[]number[] 这样需要注解数组内容类型的变量。还有像 ReactElementHTMLInputElement 以及 Express.App 这些在特定的部署环境或者有特定的依赖时才有效的类型。

TS 中最有趣的事情是你可以定义自己的类型。让我们看看一些为你的领域建模的方式。首先是使用 interface 关键字定义你的对象:

interface User {
  firstName: string;
  lastName: string;
  age: number;
  state: UserState;
}

你还可以定义 enums

enum UserState {
  ACTIVE,
  INACTIVE,
  INVITED,
}

TypeScript 甚至支持继承:

interface FunkyUser extends User {
  isDancing: boolean;
}

TypeScript 还有很多高级类型,比如联合类型,它可以用来代替枚举类型,它们的区别是,它更像 JavaScript 的书写方式。

type UserState =
  "active" |
  "inactive" |
  "invited";

const correctState: UserState = "active"; // ✅
const incorrectState: UserState = "loggedin"; // ❌ TypeError

在类型检查的时候,TypeScript 不会检查原型链或其他继承的方法,它只检查属性的类型标识以及处理对象的特性:

const bugs: User = {
  firstName: "Bugs",
  lastName: "Bunny",
  age: "too old", // TypeError: expected `number` but got `string`
  state: UserState.ACTIVE,
}

你当然可以在 TypeScript 中使用 class,我并没有让你远离它,但要注意:我会在后文解释,为什么你不需要它。看下面这个栗子:

class JavaLover implements User {
  private firstName: string;
  private lastName: string;
  private age: number;
  private state: UserState;
  
  getOpinion() {
    return [ "!!JAVA!1!" ];
  }
}

前面我们看了一些基础的 TypeScript 代码,对于它做了什么有了一个大致的了解,现在让我们来看看它在 React 中是如何使用的。

如何在 React 中使用 TypeScript?

由于 Babel 7 支持了 TypeScript 的内建,把它整合到你的构建流程变得十分简单。我仍然建议你查看构建工具的文档,其中大部分都包含有关 TypeScript 设置,写的很好的操作指南。

配置好构建流程之后,你就可以在你的组件中使用 TypeScript 了。下面是一个简单的 React-Native 组件,它接收一个 TodoItem 和一个回调函数,并展示了一条 Todo 信息。

import * as React from "react";
import { View, Text, CheckBox } from "react-native";

interface TodoItem {
  id: string;
  name: string;
  isCompleted: boolean;
}

interface TodoListItemProps {
  item: TodoItem;
  onComplete: (id: string) => void;
}

function TodoListItem(props: TodoListItemProps) {
  const { item, onComplete } = props;
  return (
    <View>
      <Text>{item.name}</Text>
      <CheckBox isChecked={item.isCompleted} onClick={state => { onComplete(item.id); }} />
    </View>
  );
}

由于 React 本质上是纯 JavaScript,因此可以像纯 JavaScript 一样编写。你只需要使用 interface(见 TodoListItemPropsinterface 定义)来定义你组件的 props 结构,并使用刚刚定义的 TodoListItemProps 类型来定义组件 props 参数的类型。你无需指定返回类型(事实上它的类型是 JSX.Element),因为可以从组件返回的表达式的类型中推断出来。

即便 JSX 不是 JavaScript 规范的一部分,TypeScript 仍然可以对它进行类型检查,这使它可以校验传入组件中的 props。

你还可以将 TypeScript 与 React 的 class API 结合使用:

import * as React from "react";

interface TimerState {
  count: number;
}

class Timer extends React.Component<{}, TimerState> {
  
  state: TimerState = {
    count: 0
  }

  timerId: number | undefined = undefined;

  componentDidMount() {
    this.timerId = setInterval(
      () => {
        this.setState(
          state => ({
            count: state.count + 1
          })
        );
      },
      1000
    );
  }

  componentWillUnmount() {
    if (this.timerId) {
      clearInterval(this.timerId);
    }
  }

  render() {
    return (
      <p>Count: {this.state.count}</p>
    )
  }
}

在你编写一个 class 时,你会将类型参数传入到你 extend 出来的 React.Component 中。首先是前面示例组件中为空的 props({} 空对象);第二个通用类型参数是组件的 state,在这里是一个只包含数字类型的 count 字段。你还会发现这里使用了初始化为 undefined 的实例字段 timerId,这也是为什么它的类型被声明为 number | undefined,这表示它可以是 numberundefined 类型。

如你所见,将 TypeScript 与 React 结合使用非常简单,不需要任何仪式。基本上,它是 prop-types 更强大的替代品,因为它支持更高级的类型,并且也可以使用普通的 JS 代码。同时,TypeScript 可以提供编译时的校验,而 prop-types 只在开发环境中有效。

如果您想自己捣鼓一些代码,可以看看我在现场演示中使用的示例。它就在这个 repo 的 todo 文件夹中:github.com/Skn0tt/reac…

为什么我们(不)应该使用 TypeScript?

到目前为止,我假设您对 TypeScript 是什么以及它可以做什么有一个大致的了解。那么接下来我会继续说明,如果我们不使用 TypeScript,那是因为什么。

不使用 TypeScript 的原因

TypeScript 并不是编写 JavaScript 的全新方式,它只是扩展了 JavaScript 编写类型注解的能力。首先,最重要的是,TypeScript 是一个类型检查器,但大家似乎有忽略这个事实的趋势,就像我最近看到的一些言论:

“TypeScript 太棒了,终究我们的 Java 开发工作也能在前端环境下进行了。”

这看起来很有意义,但实际上是有害的。没错,Java 开发者已经习惯了类型系统、class、interface 等优秀特性,但 TypeScript 对他们来说依然非常有吸引力。然而,如果 Java(或其他受面向对象编程影响较深的语言)开发者转到编写 TypeScript,又不了解 Java 与 JavaScript 之间根深蒂固的差异,那也许会带来很大的问题。记住一点:无论你的 JavaScript 代码是否由 TypeScript 编译而来,都会改变其运行时的行为。它一旦运行,就不再会有类型信息,这是它的优势,也是它的痛点。JavaScript 通过原型链继承实现了动态类型和强制类型的功能,这让它拥有了许多新的功能和特性,距离 Java 也不远了。在从 Java 开发转换到 TypeScript 开发时要一直记住这一点。

TypeScript 为何仍能帮助我,如何做到?

在项目中添加类型注解是代码结构文档化的极佳方式,它提供了一种为传递的数据注明结构的方式。TypeScript 还是“机器可读”的,这意味着它可以由机器执行!这样就可以提供强大的编辑器支持(尝试用用 VS Code 吧,它 diao bao le!),这简化了代码重构的过程,并增强了静态的安全检查。

代码结构文档化这件事,TypeScript 不仅是允许你去做,而是强制你去做。会让你停下来复盘你正在编写的代码,并思考是否能创建更清晰的代码结构以获得更高的代码质量。

至少对我来说,编写类型注解让我的代码更利于理解,因为我不需要深入研究项目就可以了解数据的结构,它消除了很多认知上的复杂性。