# 类型创建
# 类型别名
代码语言:javascript复制type A = string;
类型别名主要用于对一组类型或一个特定类型结构进行封装,以便于在其他地方进行复用。
抽离一组联合类型:
代码语言:javascript复制type StatusCode = 200 | 301 | 400 | 500;
type PossibleDataTypes = string | number | (() => unknown);
const status: StatusCode = 200;
const data: PossibleDataTypes = 123;
抽离一个函数类型:
代码语言:javascript复制type Handler = (e: Event) => void;
const clickHandler: Handler = (e) => {};
声明对象类型,像接口一样:
代码语言:javascript复制type ObjectType = {
name: string,
age: number,
}
类型别名除了用于进行特定类型的抽离封装,还能作为工具类型。工具类同样基于类型别名,只是多了个泛型。
在类型别名中,类型别名可以声明自己能接受泛型,一旦接受了泛型,就称他为工具类型:
代码语言:javascript复制type Factory<T> = T | number | string;
虽然变成了工具类型,但其基本能力仍然是创建类型,只不过工具类型能够接受泛型参数,实现更灵活的类型创建功能。
可以把工具类型理解为一个函数,泛型是入参,内部逻辑是基于传入参数进行某些操作,返回一个新的类型:
代码语言:javascript复制type Factory<T> = T | number | string;
const foo: Factory<boolean> = true; // boolean | number | string
一般不会直接使用工具类型来做类型标注,而是再声明一个新的类型别名:
代码语言:javascript复制type Factory<T> = T | number | string;
type FactoryWithBoolean = Factory<boolean>;
const foo: FactoryWithBoolean = true; // boolean | number | string
泛型参数的名称 T
也不是固定的,通常使用 T/K/U/V/M/O
等。如果为了可读性,也可以使用大驼峰形式:
type Factory<NewType> = NewType | number | string;
声明一个简单、有意义的工具类型:
代码语言:javascript复制// 接受一个类型,返回一个包括 null 的联合类型
type MaybeNull<T> = T | null;
// 可以确保处理了可能为空值的属性读取和方法调用
function process(input: MaybeNull<{ handler: () => {} }>) {
input?.handler();
}
类似的工具类:
代码语言:javascript复制type MaybeArray<T> = T | T[];
function ensureArray<T>(input: MaybeArray<T>): T[] {
if (Array.isArray(input)) {
return input;
}
return [input];
}
工具类型的主要意义是基于传入的泛型进行各种类型操作,得到一个新的类型。
# 联合类型与交叉类型
交叉类型,符号 &
,即按位与运算符。正如联合类型的 |
,它代表了按位或,即只需要符合联合类型中的一个类型即可认为实现了这个联合类型,如 A | B
只需要实现 A
或 B
即可。而代表按位与的 &
,则需要符合所有类型,才可以说实现了这个交叉类型,即 A & B
需要同时满足 A
与 B
两个类型。
interface NameStruct {
name: string;
}
interface AgeStruct {
age: number;
}
type ProfileStruct = NameStruct & AgeStruct;
const profile: ProfileStruct = {
name: 'Cell',
age: 18,
};
对于原始类型,交叉之后的类型不存在:
代码语言:javascript复制type StringOrNumber = string | number;
type StringAndNumber = string & number; // never
类型的交叉类型,其内部同名属性类型同样会按照交叉类型进行合并:
代码语言:javascript复制type Struct1 = {
primitiveProp: string;
objectProp: {
name: string;
};
}
type Struct2 = {
primitiveProp: number;
objectProp: {
age: number;
};
}
type Struct3 = Struct1 & Struct2;
type PrimitivePropType = Struct3['primitiveProp']; // string & number => never
type ObjectPropType = Struct3['objectProp']; // { name: string } & { age: number } => { name: string; age: number }
如果是两个联合类型组成的交叉类型:
代码语言:javascript复制type UnionIntersection1 = (1 | 2 | 3) & (1 | 2); // 1 | 2
type UnionIntersection2 = (1 | 2 | 3) & (4 | 5); // never
type UnionIntersection3 = (string | number | symbol) & string; // string
交叉类型和联合类型的区别,联合类型只需要符合成员之一即可,交叉类型需要严格符合每一位成员。
# 索引类型
索引类型指的不是某一特定的类型工具,它其实包含三个部分:索引签名类型、索引类型查询、索引类型访问。它们都通过索引的形式来进行类型操作,但索引签名类型是声明,后两者都是读取。
# 索引签名类型
索引签名类型主要指在接口或类型别名中,通过以下语法快速声明一个键值类型一直的类型结构:
代码语言:javascript复制interface AllStringTypes {
[key: string]: string;
}
type AllStringTypes = {
[key: string]: string;
}
即使没有声明具体的属性,对于这些类型结构的属性访问将被视为 string
类型:
interface AllStringTypes {
[key: string]: string;
}
type PropType1 = AllStringTypes['foo']; // string
type PropType2 = AllStringTypes['bar']; // string
注意,声明的键的类型为 string
,意味着在实现这个类型结构的变量中只能声明字符串类型的键:
interface AllStringTypes {
[key: string]: string;
}
const foo: AllStringTypes = {
'foo': 'foo',
};
但由于 JavaScript 中,对于 obj[prop]
形式的访问会将数字索引转换为字符串索引访问,即 obj[2022]
和 obj['2022']
效果一致。因此,在字符串索引签名类型中仍然可以声明数字类型的键。类似的,symbol
类型也是如此。
const foo: AllStringTypes = {
'foo': 'foo',
2022: '2022',
[Symbol('foo')]: 'symbol',
};
索引签名类型也可以和具体的键值对类型声明并存,但是必须满足具体的键值类型也符合索引签名类型的声明:
代码语言:javascript复制interface AllStringTypes {
foo: string;
[key: string]: string;
}
// 也包括联合类型
interface StringOrBooleanTypes {
propA: number;
propB: boolean;
[key: string]: string | boolean;
}
索引签名类型常见场景是在重构 JavaScript 代码时,为内部属性较多的对象声明一个 any
的索引类型签名,以此来暂时支持对类型未明确属性的访问,并在后续中逐渐补全类型。
# 索引类型查询
keyof
,可以将对象中的所有键转换为对应字面量类型,然后在组合成联合类型。**这里不会将数字类型的键名转换为字符串类型字面量,而是仍然保持数字类型字面量”:
interface Foo {
bar: 1,
2022: 2,
}
type FooKeys = keyof Foo; // 'bar' | 2022
除了应用于已知的对象类型结构上之外,可以直接 keyof any
来产生一个联合类型——由所有可用作对象键值的类型组成:string | number | symbol
。
keyof
的产物必定是一个联合类型。
# 索引类型访问
在 JavaScript 中可以通过 obj[expression]
方式来动态访问一个对象属性(即计算属性),expression
表达式会先被执行,然后使用返回值来访问属性。在 TypeScript 中,也可以使用类似方式,但是 expression
需要换成类型:
interface NumberRecord {
[key: string]: number;
}
type PropType = NumberRecord[string]; // number
// 更直观的例子
interface Foo {
propA: number;
propB: boolean;
}
// 'propA' 和 'propB' 都是字符串字面量类型,而不是字符串值
type PropAType = Foo['propA']; // number
type PropBType = Foo['propB']; // boolean
索引类型查询的本质就是,通过键的字面量类型(propA
)访问这个键对应的键值类型(number
)。
可以使用 keyof
一次性获取这个对象所有的键的字面量类型:
interface Foo {
propA: number;
propB: boolean;
propC: string;
}
type PropTypeUnion = Foo[keyof Foo]; // number | boolean | string
使用字面量联合类型进行索引类型访问,其结果就是将联合类型每个分支对应的类型进行访问后的结果,重新组装成联合类型。
注意,在未声明索引签名类型的情况下,不能使用 NumberRecord[string]
这种原始类型的访问方式,而只能通过键名的字面量类型来进行访问。
interface Foo {
propA: number;
}
type PropAType = Foo['propA']; // number
// type PropAType2 = Foo[string]; // error
// Type 'Foo' has no matching index signature for type 'string'.
# 映射类型
映射类型指的是一个确切的类型工具,主要作用即是基于键名映射到键值类型。
代码语言:javascript复制type Stringify<T> = {
[K in keyof T]: string
};
这个工具类型接受一个对象类型,使用 keyof
获得对象类型的键名组成字面量联合类型,然后通过映射类型(in
关键字)将这个联合类型的每一个成员映射出来,并将其键值类型设置为 string
。
interface Foo {
prop1: string;
prop2: number;
prop3: boolean;
prop4: () => void;
}
type StringifiedFoo = Stringify<Foo>;
// 等价于
// interface StringifiedFoo {
// prop1: string;
// prop2: string;
// prop3: string;
// prop4: string;
// }
既然拿到了键,那键值类型也可以拿到:
代码语言:javascript复制type Clone<T> = {
[K in keyof T]: T[K];
};
interface Foo {
prop1: string;
prop2: number;
}
type ClonedFoo = Clone<Foo>;
// 等价于
// interface ClonedFoo {
// prop1: string;
// prop2: number;
// }
T[K]
即索引类型访问,K in
属于映射类型语法,keyof T
属于 keyof
操作符,[K in keyof T]
整体 [*]
属于索引签名类型,T[K]
属于索引类型访问。
类型工具 | 创建新类型的方式 | 常见搭配 |
---|---|---|
类型别名 | 将一组类型/类型结构封装,作为一个新的类型 | 联合类型、映射类型 |
工具类型 | 在类型别名的基础上,基于泛型去动态创建类型 | 使用类型工具 |
联合类型 | 创建一组类型集合,满足其中一个类型即满足这个联合类型(|) | 类型别名、工具类型 |
交叉类型 | 创建一组类型集合,满足其中所有类型才满足映射联合类型(&) | 类型别名、工具类型 |
索引签名类型 | 声明一个拥有任意属性,键值类型一致的接口结构 | 映射类型 |
索引类型查询 | 从一个接口结构,创建一个由其键名字符串字面量组成的联合类型 | 映射类型 |
索引类型访问 | 从一个接口结构,使用键名字符串字面量访问到对应的键值类型 | 类型别名、映射类型 |
映射类型 | 从一个联合类型依次映射到其内部的每一个类型 | 工具类型 |
# 类型安全保护
# 类型查询
TypeScript 存在两种功能不同的 typeof
操作符,常见的是 JavaScript 中用于检查变量类型的 typeof
,它会返回 'string' / 'number' / 'object' / 'undefined'
等值。在 TypeScript 中,还新增了用于类型查询的 typeof
操作符,它会返回一个 TypeScript 类型:
const str = 'Cell';
const obj = { name: 'Cell' };
const nullVar = null;
const undefinedVar = undefined;
const func = (input: string) => {
return input.length > 10;
};
type Str = typeof str; // string
type Obj = typeof obj; // { name: string }
type Null = typeof nullVar; // null
type Undefined = typeof undefinedVar; // undefined
type Func = typeof func; // (input: string) => boolean
不仅可以直接在类型标注中使用 typeof
,还能在工具类型中使用 typeof
:
const func = (input: string) => {
return input.length > 10;
};
const func2: typeof func = (name: string) => {
return name === 'Cell';
};
大部分情况下,typeof
返回的类型就是鼠标悬浮在变量名上时出现的推导后的类型,并且是最窄的推导程度(即到字面量类型的级别)。
# 类型守卫
TypeScript 提供了非常强大的类型推导能力,会随代码逻辑不断尝试收窄类型,这种能力称为类型的控制流分析。
TypeScript 引入了 is 关键字来显式地提供类型信息:
代码语言:javascript复制function isString(input: unknown): input is string {
return typeof input === 'string';
}
function foo(input: string | number) {
if (isString(input)) {
// input: string
} else {
// input: number
}
}
isString
函数称为类型守卫,在它的返回值中,不在使用 boolean
作为类型标注,而是使用 input is string
:
input
是函数的某个参数is string
即is 预期类型
,如果这个函数成功返回ture
,那么is
前参数的类型,就会被这个类型守卫调用方后续的类型控制流分析收集到。
注意,类型守卫函数中并不会对判断逻辑和实际类型的关联进行检查,会信任开发者的指定:
代码语言:javascript复制function isString(input: unknown): input is number {
return typeof input === 'string';
}
function foo(input: string | number) {
if (isString(input)) {
(input).length; // error: number 没有 length 属性
} else {
// input: number
}
}
可以在类型守卫中使用对象类型、联合类型:
代码语言:javascript复制type Falsy = false | 0 | '' | null | undefined;
const isFalsy = (val: unknown): val is Falsy => !val;
type Primitive = string | number | boolean | undefined;
const isPrimitive = (val: unknown): val is Primitive => {
return ['string', 'number', 'boolean', 'undefined'].includes(typeof val);
}
# 基于 in 与 instanceof 的类型保护
in
是 JavaScript 中已有的部分,可以通过 key in object
来判断 key
是否存在于 object
或其原型链上。
在 TypeScript 中,in
也可以用于类型保护:
interface Foo {
foo: string;
fooOnly: boolean;
shared: number;
}
interface Bar {
bar: string;
barOnly: boolean;
shared: number;
}
function handle(input: Foo | Bar) {
if ('foo' in input) {
input.fooOnly; // ok
} else {
input.barOnly; // ok
}
}
各个类型独有的数学可以作为可辨识属性,存在具有区分能力的辨识属性称为可辨识联合类型。
可辨识属性可以使结构层面的,如 结构 A
的属性 prop
是数组,而 结构 B
的属性 prop
是对象,这样就可以通过 prop
的类型来区分 结构 A
和 结构 B
。甚至可以是共同属性的字面量类型差异:
function ensureArray(input: number | number[]): number[] {
if (Array.isArray(input)) {
return input;
} else {
return [input];
}
}
interface Foo {
kind: 'foo';
diffType: string;
fooOnly: boolean;
shared: number;
}
interface Bar {
kind: 'bar';
diffType: number;
barOnly: boolean;
shared: number;
}
// 对于同名但不同类型的属性,需要使用字面量类型来区分
function handle(input: Foo | Bar) {
if (input.kind === 'foo') {
input.fooOnly; // ok
} else {
input.barOnly; // ok
}
}
instanceof
在 JavaScript 中用于判断原型级别的关系,TypeScript 中也可以用于类型保护:
class FooBase {}
class BarBase {}
class Foo extends FooBase {
fooOnly() {}
}
class Bar extends BarBase {
barOnly() {}
}
function handle(input: FooBase | BarBase) {
if (input instanceof Foo) {
input.fooOnly(); // ok
} else {
input.barOnly(); // ok
}
}
# 类型断言守卫
代码语言:javascript复制import assert from 'assert';
let name: any = 'Cell';
assert(typeof name === 'number'); // throw error
name.toFixed();
断言守卫和类型守卫最大的不同在于,在判断条件不通过时,断言守卫需要抛出一个错误,类型守卫只需要剔除掉预期的类型。