跳到主要内容

5. 类型系统

5.1. 类型推导​

对于所有类型的表达式(包括变量、字段、返回值,等等),都可以依赖 TypeScript 编译器所实现的类型推导。

const x = 15;  // x 的类型可以推导得出.
可选

当变量或参数被初始化为 string , number , boolean , RegExp 正则表达式字面量或 new 表达式时,由于明显能够推导出类型,因此应当省略类型记号。

// 可以省略 boolean ,添加 boolean 记号对提高可读性没有任何帮助!
const x: boolean = true;
// 不要这样做!Set 类型显然可以从初始化语句中推导得出。
const x: Set<string> = new Set();
// 应当这样做!依赖 TypeScript 的类型推导。
const x = new Set<string>();
提示

对于更为复杂的表达式,类型记号有助于提高代码的可读性。

5.1.1. 返回类型​

应当

TypeScript 会自动推导方法的返回值类型,但是对难以理解的复杂返回类型应当使用类型记号进行阐明。

5.2. null 还是 undefined ?​

TypeScript 支持 null 和 undefined 类型。可空类型可以通过联合类型实现,例如 string | null 。对于 undefined 也是类似的。对于 null 和 undefined 的联合类型,并无特殊的语法。

TypeScript 代码中可以使用 undefined 或者 null 标记缺少的值,这里并无通用的规则约定应当使用其中的某一种。许多 JavaScript API 使用 undefined (例如 Map.get ),然而 DOM API 中则更多地使用 null (例如 Element.getAttribute ),因此,对于 null 和 undefined 的选择取决于当前的上下文。

应当

对于引用类型的空值应当使用 null,对于基本类型的空值应当使用 undefined。

5.2.1. 可空/未定义类型别名​

提案

不应为包括 |null 或 |undefined 的联合类型创建类型别名。这种可空的别名通常意味着空值在应用中会被层层传递,并且它掩盖了导致空值出现的源头。另外,这种别名也让类或接口中的某个值何时有可能为空变得不确定。

提案

因此,代码必须在使用别名时才允许添加 |null 或者 |undefined 。同时,代码应当在空值出现位置的附近对其进行处理。

// 不要这样做!不要在创建别名的时候包含 undefined !
type CoffeeResponse = Latte | Americano | undefined;

class CoffeeService {
getLatte(): CoffeeResponse {
};
}
// 应当这样做!在使用别名的时候联合 undefined !
type CoffeeResponse = Latte | Americano;

class CoffeeService {
getLatte(): CoffeeResponse | undefined {
};
}

5.2.2. 可选参数(?) 还是 undefined 类型?​

TypeScript 支持使用 ? 创建可选参数和可选字段,例如:

interface CoffeeOrder {
sugarCubes: number;
milk?: Whole | LowFat | HalfHalf;
}

function pourCoffee(volume?: Milliliter) {
}

可选参数实际上隐式地向类型中联合了 |undefined 。不同之处在于,在构造类实例或调用方法时,可选参数可以被直接省略。例如, {sugarCubes: 1} 是一个合法的 CoffeeOrder ,因为 milk 字段是可选的。

应当

应当使用可选字段(对于类或者接口)和可选参数而非联合 |undefined 类型。

应当

对于类,应当尽可能避免使用可选字段,尽可能初始化每一个字段。

5.3. 结构类型 与 指名类型​

TypeScript 的类型系统使用的是结构类型而非指名类型。具体地说,一个值,如果它拥有某个类型的所有属性,且所有属性的类型能够递归地一一匹配,则这个值与这个类型也是匹配的。

提案

在提供基于结构类型的实现时,应当在符号的声明位置显式地包含其类型,使类型检查和错误检测能够更准确地工作。

// 不推荐这样做!
const badFoo = {
a: 123,
b: 'abc',
}
// 推荐这样做!
const foo: Foo = {
a: 123,
b: 'abc',
}

5.4. interface 还是 type?​

TypeScript 支持使用 type 为类型命名。这一功能可以用于基本类型、联合类型、元组以及其它类型。

应当

对于用于对象的类型时,应当使用 interface ,而非对象字面量表达式的 type。

// 不要这样做!
type User = {
firstName: string,
lastName: string,
}
// 应当这样做!
interface User {
firstName: string;
lastName: string;
}
提示

区别:

  • type 不可重叠,作用域内唯一。interface 可重叠。此特性可以极其方便地对全局变量、第三方库的类型做扩展。type 支持组合类型,交叉类型,而接口类型无法覆盖。
  • interface 可被继承和实现,type 不行。
  • interface 只能声明对象,而 type 可以声明元组、联合类型、交叉类型、原始类型,也包括对象。

5.5. Array<T> 类型​

必须

对于简单类型(名称中只包含字母、数字和点 . 的类型),应当使用数组的语法糖 T[] ,而非更长的 Array<T> 形式。

对于其它复杂的类型,则应当使用较长的 Array<T> 。

这条规则也适用于 readonly T[] 和 ReadonlyArray<T> 。

// 不要这样做!
const f: Array<string>; // 语法糖写法更短。
const g: ReadonlyArray<string>;
const h: { n: number, s: string }[]; // 大括号和中括号让这行代码难以阅读。
const i: (string | number)[];
const j: readonly (string | number)[];
// 应当这样做!
const a: string[];
const b: readonly string[];
const c: ns.MyObj[];
const d: Array<string | number>;
const e: ReadonlyArray<string | number>;

5.6. 索引类型 {[key: string]: number}​

可选

在 TypeScript 中,应当为键提供一个有意义的标签名。(当然,这个标签只有在文档中有实际意义,在其它场合是无用的。)

// 不要这样做!
const users: { [key: string]: number } = {name: 'foo'};
// 应当这样做!
const users: { [userName: string]: number } = {name: 'foo'};

相比使用上面的这种形式,在 TypeScript 中可以考虑使用 ES6 新增的 Map 与 Set 类型。Map 类型的键和 Set 类型的元素都允许使用 string 以外的其他类型。

提示

TypeScript 内建的 Record<Keys, ValueType> 允许使用已定义的一组键创建类型。它与关联数组的不同之处在于键是静态确定的。

5.7. 映射类型与条件类型​

TypeScript 中的 映射类型 与 条件类型 让程序员能够在已有类型的基础上构建出新的类型。在 TypeScript 的标准库中有许多类型运算符都是基于这一机制(例如 Record 、 Partial 、 Readonly 等等)。

例如 TypeScript 为我们提供了一些工具函数:

Partial 实现:

将一个类型的所有属性变为可选。

type Partial<T> = {
[P in keyof T]?: T[P];
};

Required 实现:

将一个类型的所有属性变为必选。

type Required<T> = {
[P in keyof T]-?: T[P]; // - 号表示去除可选属性
};

Readonly 实现:

将一个类型的所有属性变为只读。

type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

Pick 实现:

从给定的类型中选取指定的键值,然后组成一个新的类型。

type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

Record 实现:

使用传入的泛型参数分别作为接口类型的属性和值,生成接口类型。

type Record<K extends keyof any, T> = {
[P in K]: T;
};

5.8. any 类型​

应当

应当尽可能减少项目上 any 出现的频率,可以尝试使用 unknown 类型。

// 不要这样做!
const danger: any = value /* 这是任意一个表达式的结果 */;
danger.whoops(); // 完全未经检查的访问!
// 应当这样做!
// 可以将任何值(包括 null 和 undefined)赋给 val,
// 但在缩窄类型或者类型转换之前并不能使用它。
const val: unknown = value;

5.9. 包装类型​

不应

不要使用如下几种类型,它们是 JavaScript 中基本类型的包装类型:

  • String 、 Boolean 和 Number 。它们的含义和对应的基本类型 string 、 boolean 和 number 略有不同。任何时候,都应当使用后者。
  • Object 。它和 {} 与 object 类似,但包含的范围略微更大。应当使用 {} 表示“包括除 null 和 undefined 之外所有类型”的类型,使用 object 表示“所有基本类型以外”的类型(这里的“所有基本类型”包括上文中提到的基本类型, symbol 和 bigint )。

5.10. 只有泛型的返回类型​

不要创建返回类型只有泛型的 API。如果现有的 API 中存在这种情况,使用时应当显式地标明泛型参数类型。

5.11. 类型操作符​

目前项目上支持的类型操作符有:

  • typeof、instanceof:返回更详细的类型
  • keyof:返回一个对象的属性名称的字符串数组
  • O[K]:返回对象 K 的值
  • [K in O]:逐一映射 O 的类型
  • + 、 - 、 readonly 、 ?:添加、删除、只读、可选的类型修饰符
  • x ? Y : Z:泛型、类型别名、函数参数类型的条件类型判断
  • !: 非空断言
  • =: 泛型的默认值
  • as: 类型断言
  • is: 类型谓词,辅助类型推断
此篇维护者:黄振敏