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