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: 类型谓词,辅助类型推断
