Typescript 中的泛型参数默认值

2021-01-25

Typescript 2.3 实现了泛型参数默认值,它允许你给泛型的参数类型设置默认类型。

在这篇文章中,我会通过将下面的 React 组件从 Javascript(即 JSX) 转成 Typescript(即 TSX) 来展开我们如何能够从泛型参数默认值中获益:

class Greeting extends React.Component {
  render() {
    return <span>Hello, {this.props.name}!</span>;
  }
}

不用担心,你不用懂 React 也能看懂下面的内容。

给这个类组件创建一个类型定义

让我们先来给 Component 这个类创建一个类型定义。每一个 React 类组件都有两个属性 popsstate,它们的类型都是任意的。所以我们可以定义如下的类型:

declare namespace React {
  class Component {
    props: any;
    state: any;
  }
}

注意为了说明的目的这个例子中做了极大的简化。毕竟,这篇文章不是在讲 React,而是关于泛型参数和它的默认值。真实的DefinitlyTyped 上的 React 类型定义 要复杂得多。

现在我们得到了类型检查和自动补全建议:

class Greeting extends React.Component {
  render() {
    return <span>Hello, {this.props.name}!</span>;
  }
}

我们可以像下面这样创建一个组件实例:

<Greeting name="World" />

渲染我们的组件会生成下面的 HTML,和我们期望的一样:

<span>Hello, World!</span>

目前来看一切都很好。

Props 和 State 使用泛型参数

虽然上面的例子可以编译和运行正常,但 Componnent 的类型定义非常不准确。因为我们将 propsstate 定义为 any 类型,Typescript 编译器并帮不了太多的忙。

让我们来把它们变得更具体一些。我们会引入两个泛型 PropsState 来准确地描述 propsstate 两个属性的形状:

declare namespace React {
  class Component<Props, State> {
    props: Props;
    state: State;
  }
}

我们再来创建一个 GreetingProps 类型,它定义了一个 string 类型的 name 属性,并且作为实参传给 Props 类型参数:

type GreetingProps = { name: string };

class Greeting extends React.Component<GreetingProps, any> {
  render() {
    return <span>Hello, {this.props.name}!</span>;
  }
}

术语解释:

  • GreetingPropsProps 参数的类型实参
  • 类似的,anyState 参数的类型实参

有了这些类型,我们的组件会得到更好的类型检查和自动补全建议:

然而,如果我们想要继承 React.Componnet 类,我们现在必须提供两个类型。我们最初的代码不再是类型正确的:

// Error: Generic type 'Component<Props, State>'
// requires 2 type argument(s).
class Greeting extends React.Component {
  render() {
    return <span>Hello, {this.props.name}!</span>;
  }
}

如果我们不想提供类似 GreetingProps 这样具体的类型,我们可以通过提供 any 类型(或者其他不太有用的类型比如 {})给 PropsState 来修复代码:

class Greeting extends React.Component<any, any> {
  render() {
    return <span>Hello, {this.props.name}!</span>;
  }
}

这种方式可以正常工作并且通过类型检查,但是假如 any 在这个例子中是默认值,并且可以简单地不用传任何实参不是更好吗?好,那我们就来介绍泛型参数默认值。

拥有参数默认值的泛型类型定义

从 Typescript 2.3 开始,我们可以选择性地给我们的泛型类型参数添加一个默认的类型。在我们的例子中,我们可以将 PropsState 的默认值设置为 any 类型,如果没有显式地设置类型实参,就会使用该默认值:

declare namespace React {
  class Component<Props = any, State = any> {
    props: Props;
    state: State;
  }
}

通过这种方式,我们最初始的代码再一次通过了类型检查并且成功编译:

class Greeting extends React.Component {
  render() {
    return <span>Hello, {this.props.name}!</span>;
  }
}

当然,我们依然可以给 Props 类型参数提供类型实参,它会覆盖默认的 any 类型,就像我们之前做过的一样:

type GreetingProps = { name: string };

class Greeting extends React.Component<GreetingProps, any> {
  render() {
    return <span>Hello, {this.props.name}!</span>;
  }
}

我们还可以做些其他有趣的事。现在这两个参数都有了默认的类型,这让它们的实参变得可选——你可以不用提供实参。这允许我们给 Props 显式地设置一个类型,同时让 State 隐式地使用默认的 any 类型。

type GreetingProps = { name: string };

class Greeting extends React.Component<GreetingProps> {
  render() {
    return <span>Hello, {this.props.name}!</span>;
  }
}

注意我们只提供了一个类型实参。我们可以选择性地不给右边的参数传实参。也就是说,在这个例子中,我们不能给 State 设置类型实参同时让 Props 使用默认参数。类似地,在定义类型的时候,可选的类型参数不能放在必需参数的前面。

另一个例子

在我的上一篇文章 Typescript 中的 mixin 类 中,我一开始就定义了下面的两个类型别名:

type Constructor<T> = new (...args: any[]) => T;
type Constructable = Constructor<{}>;

Constructable 类型是单纯的语法糖。它可以替代 Constructor<{}>,这样我们不用每次都写泛型类型参数。那有了泛型类型默认值,我们完全可以不用写 Constructable 类型,只需要将 {} 设为默认值即可:

type Constructor<T = {}> = new (...args: any[]) => T;

这个语法相对复杂一些,但代码变得更简洁了。很好!

郑超的独立博客