跳到主要内容

5. 类型系统

5.1. 类型推导

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

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

当变量或参数被初始化为 stringnumberbooleanRegExp 正则表达式字面量或 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 支持 nullundefined 类型。可空类型可以通过联合类型实现,例如 string | null 。对于 undefined 也是类似的。对于 nullundefined 的联合类型,并无特殊的语法。

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

应当

对于引用类型的空值应当使用 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 新增的 MapSet 类型。Map 类型的键和 Set 类型的元素都允许使用 string 以外的其他类型。

提示

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

5.7. 映射类型与条件类型

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

例如 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 中基本类型的包装类型:

  • StringBooleanNumber 。它们的含义和对应的基本类型 stringbooleannumber 略有不同。任何时候,都应当使用后者。
  • Object 。它和 {}object 类似,但包含的范围略微更大。应当使用 {} 表示“包括除 nullundefined 之外所有类型”的类型,使用 object 表示“所有基本类型以外”的类型(这里的“所有基本类型”包括上文中提到的基本类型, symbolbigint )。

5.10. 只有泛型的返回类型

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

5.11. 类型操作符

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

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