类型别名

如同接口声明能够为对象类型命名,类型别名声明则能够为 TypeScript 中的任意类型命名。

类型别名声明

类型别名声明能够定义一个类型别名,它的基本语法如下所示:

type AliasName = Type

在该语法中,type 是声明类型别名的关键字;AliasName 表示类型别名的名称;Type 表示类型别名关联的具体类型。

类型别名的名称必须为合法的标识符。由于类型别名表示一种类型,因此类型别名的首字母通常需要大写。同时需要注意,不能使用 TypeScript 内置的类型名作为类型别名的名称,例如 booleannumberany 等。下例中,我们声明了一个类型别名 Point,它表示包含两个属性的对象类型:

type Point = { x: number; y: number };

类型别名引用的类型可以为任意类型,例如原始类型、对象类型、联合类型和交叉类型等。示例如下:

type StringType = string;

type BooleanType = true | false;

type Point = { x: number; y: number; z?: number };

在类型别名中,也可以引用其他类型别名。示例如下:

type Numeric = number | bigint;

// string | number | bigint
type StringOrNumber = string | Numeric;

类型别名不会创建出一种新的类型,它只是给已有类型命名并直接引用该类型。在程序中,使用类型别名与直接使用该类型别名引用的类型是完全等价的。因此,在程序中可以直接使用类型别名引用的类型来替换掉类型别名。示例如下:

type Point = { x: number; y: number };

let a: Point;
// let a: { x: number; y: number };

在程序中,可能会有一些比较复杂的或者书写起来比较长的类型,这时我们就可以声明一个类型别名来引用该类型,这也便于我们对这个类型进行重用。例如,下例中的 DecimalDigit 类型比较长,如果在每个引用该类型的地方都完整地写出该类型会很不方便。使用类型别名不但能够简化代码,还能够给该类型起一个具有描述性的名字。示例如下:

type DecimalDigit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;

const digit: DecimalDigit = 6;

递归的类型别名

一般情况下,在类型别名声明中赋值运算符右侧的类型不允许引用当前定义的类型别名。因为类型别名对其引用的类型使用的是及早求值的策略,而不是惰性求值的策略。因此,如果类型别名引用了自身,那么在解析类型别名时就会出现无限递归引用的问题。示例如下:

type T = T;
//   ~
//   编译错误!类型别名 'T' 存在循环的自身引用

在 TypeScript 3.7 版本中,编译器对类型别名的解析进行了一些优化。在类型别名所引用的类型中,使用惰性求值的策略来解析泛型类型参数。因此,允许在泛型类型参数中递归地使用类型别名。总结起来,目前允许在以下场景中使用递归的类型别名:

  1. 若类型别名引用的类型为接口类型、对象类型字面量、函数类型字面量和构造函数类型字面量,则允许递归引用类型别名。示例如下:

    type T0 = { name: T0 };
    type T1 = () => T1;
    type T2 = new () => T2;
  2. 若类型别名引用的是数组类型或元组类型,则允许在元素类型中递归地引用类型别名。示例如下:

    type T0 = Array<T0>;
    
    type T1 = T1[];
    
    type T3 = [number, T3];
  3. 若类型别名引用的是泛型类或泛型接口,则允许在类型参数中递归的引用类型别名。关于泛型的详细介绍请参考 6.1 节。示例如下:

    interface A<T> {
        name: T;
    }
    type T0 = A<T0>;
    
    class B<T> {
        name: T | undefined;
    }
    type T1 = B<T1>;

通过递归的类型别名能够定义一些特别常用的类型。TypeScript 官方文档中给出了使用递归的类型别名来定义 Json 类型的例子,示例如下:

type Json =
    | string
    | number
    | boolean
    | null
    | { [property: string]: Json }
    | Json[];

const data: Json = {
    name: 'TypeScript',
    version: { major: 3 }
};

类型别名与接口

类型别名与接口相似,它们都可以给类型命名并通过该名字来引用表示的类型。虽然在大部分场景中两者是可以互换使用的,但类型别名和接口之间还是存在一些差别。

区别之一,类型别名能够表示非对象类型,而接口则只能表示对象类型。因此,当我们想要表示原始类型、联合类型和交叉类型等类型时只能使用类型别名。示例如下:

type NumericType = number | bigint;

区别之二,接口可以继承其他的接口、类等对象类型,而类型别名则不支持继承。示例如下:

interface Shape {
    name: string;
}

interface Circle extends Shape {
    radius: number;
}

若要对类型别名实现类似继承的功能,则需要使用一些变通方法。例如,当类型别名表示对象类型时,可以借助于交叉类型来实现继承的效果。示例如下:

type Shape = { name: string };

type Circle = Shape & { radius: number };

function foo(circle: Circle) {
    const name = circle.name;
    const radius = circle.radius;
}

此例中的方法只适用于表示对象类型的类型别名。如果类型别名表示非对象类型,则无法使用该方法。关于交叉类型的详细介绍请参考 6.4 节。

区别之三,接口名总是会显示在编译器的诊断信息(例如,错误提示和警告)和代码编辑器的智能提示信息中,而类型别名的名字只在特定情况下才会显示出来。示例如下:

type NumericType = number | bigint;

interface Circle {
    radius: number;
}

function f(value: NumericType, circle: Circle) {
    const bar: boolean = value;
    //    ~~~
    //    编译错误!错误消息如下:
    //    Type 'number | bigint' is not assignable to
    //        type 'boolean'.

    const baz: boolean = circle;
    //    ~~~
    //    编译错误!错误消息如下:
    //    Type 'Circle' is not assignable to type 'boolean'.
}

此例中,分别定义了 NumericType 类型别名和 Circle 接口。在 f 函数中,我们有意制造了两个和它们有关的类型错误。第 11 行,与类型别名有关的错误消息没有显示出类型别名的名字,而是将类型别名表示的具体类型展开显示,即 number | bigint 联合类型。第 17 行,在与接口有关的错误消息中直接显示了接口的名字 'Circle'

只有当类型别名表示数组类型、元组类型以及类或接口的泛型实例类型时,才会在相关提示信息中显示类型别名的名字。示例如下:

type Point = [number, number];

function f(value: Point) {
    const bar: boolean = value;
    //    ~~~
    //    编译错误!错误消息如下:
    //    Type 'Point' is not assignable to type 'boolean'.
}

区别之四,接口具有声明合并的行为,而类型别名则不会进行声明合并。示例如下:

interface A {
    x: number;
}
interface A {
    y: number;
}

此例中,定义了两个同名接口 A,最终这两个接口中的类型成员会被合并。合并后的接口 A 如下所示:

interface A {
    x: number;
    y: number;
}

关于声明合并的详细介绍请参考 7.10 节。