TypeScript 笔记
第一章:走进 TS 世界
一、是什么?
TypeScript 简称 TS,是 JavaScript 的超集(JS 有的,TS 都有)。所有的 JS 代码都是 TS 代码。
TypeScript = Type + JavaScript(在 JS 基础上,加了类型支持)。
TypeScript 是微软开发的开源编程语言,可以在任何运行 JavaScript 的地方运行。
1. 为什么有 TS 之类的工具?
JS 在运行代码前,其实不知道函数调用的结果。类似:
message.toLowerCase();
// message 是否能调用?
// message 是否有一个 toLowerCase 属性?
// 如果有 toLowerCase 是可以被调用的吗?
// 如果可以被调用,会返回什么?因此,就需要静态类型系统,可以在 JS 运行前先计算出值的类型(包括值的类型,有什么属性和方法),在决定是否执行程序。还可以代码补全(类型检查器有了类型信息,可以在你输入的时候列出可使用的属性、方法)、快速修复。
2. TS 能干什么?
显示类型
// string, Date就是相应person,date的注解(签名),当不符合定义的类型时就会抛出错误
function greet(person: string, date: Date)同时,TS 类型系统可以正确推断出一些标识符相应的类型。
类型抹除
TypeScript 代码经过 TSC 转换为 JavaScript 代码。
降级
将高版本的 ECMAScript 语法转为低版本的过程就叫做降级,默认转化为 ES3。
tsc --target es2015 xxx.ts 指定版本。
严格模式
TS 赋予了用户调整代码检查的颗粒度。在 TypeScript 的配置文件 tsconfig.json 中进行配置。
"strict": true严格模式。- noImplicitAny 选项设置为 true,表示在 TypeScript 代码中,如果一个变量的类型推断为 any,但没有明确指定类型,那么 TypeScript 编译器将报错。
- strictNullChecks 选项设置为 true,表示在 TypeScript 代码中,null 和 undefined 值将被严格检查,不能赋值给其他类型的变量,除非这个变量的类型明确包含了 null 或 undefined。
……
3. 快速入门
1)安装
npm install -g typescript2)创建一个 TS 文件
3)使用 tsc 对 TS 文件进行编译(网页不认 TS 文件,这里要使用编译器将 ts 文件转换成 JS 文件)
执行命令:tsc xxx.ts
运行成功时,会在编译的 TS 文件下产生一个 JS 文件;否则,报相关的错误。错误时仍旧生成 JS 文件。
使用 tsc --noEmitOnError:noEmitOnError 在发生错误时不生成输出。
4)运行编译好的 JS 文件。node xxx.js
简化 TS 运行的步骤
问题:每次都要先生成 JS,然后执行 JS。
简化方式:使用 ts-node 包,直接在 Node.js 中执行 TS 代码。
首先需要安装它。
npm install -g ts-node
# 或者
yarn global add ts-node使用 ts-node 命令来运行 TypeScript 文件。
ts-node my-script.ts第二章:类型系统
TypeScript 是 JS 的超集,TS 提供了 JS 的所有功能,并且额外的增加了类型系统。
JS 有类型(比如 number / string 等),但是 JS 不会检查变量的类型是否发生变化。而 TS 会检查。
TypeScript 类型系统的主要优势:可以显示标记出代码中的意外行为,从而降低了发生错误的可能性。
JS 已有类型
原始类型:number boolean string null undefined symbol bigInt
对象类型:函数、object (包括 array)
TS 新增类型
联合类型、自定义类型、接口、元组 (tuple) 、字面量类型、枚举 (enum) 、void、any、unknown、never 等。
类型注解
当使用 const、var 或 let 声明一个变量时,可以选择性的添加一个类型注解,显式指定变量的类型:
typescriptlet myName: string = "Alice";不过大部分时候,这不是必须的。因为 TypeScript 会自动推断类型。
一、基础类型
number、boolean、string、null、undefined、symbol、bigint、object。
const name: string = 'yxts';
const age: number = 24;
const male: boolean = false;
const undef: undefined = undefined;
const nul: null = null;
const obj: object = { name, age, male };
const bigintVar1: bigint = 9007199254740991n;
const bigintVar2: bigint = BigInt(9007199254740991);
const symbolVar: symbol = Symbol('unique');注意:
null、undefined 在没有开启 strictNullChecks 检查的情况下,会被视作其他类型的子类型,比如 string 类型会被认为包含了 null 与 undefined 类型。
typescriptx const tmp1: null = null; const tmp2: undefined = undefined; const tmp3: string = null; // 仅在关闭 strictNullChecks 时成立,下同 const tmp4: string = undefined;
二、特殊类型
void、any、unknown、never。
1. void
如果函数没有返回值,那么,函数返回值类型为:void
function greet(name: string): void {
console.log('hello', name)
}注意:
void 是 TS 新增的。
一个函数没返回值,不仅可以标注 void,也可以标注 null (需要在关闭 strictNullChecks 配置的情况下才能成立)、undefined。
typescriptconst voidVar1: void = undefined; const voidVar2: void = null; // 需要关闭 strictNullChecks
2. any
TypeScript 有一个特殊的类型 any,当不希望一个值导致类型检查错误的时候,就可以设置为 any。
当一个值是 any 类型的时候,可以获取它的任意属性(也会被转为 any 类型),或者像函数一样调用它,把它赋值给一个任意类型的值,或者把任意类型的值赋值给它,再或者是其他语法正确的操作,都可以:
let obj: any = { x: 0 };
// 以下任何一行代码都不会引发编译器错误。
// 使用“any”将禁用所有进一步的类型检查,并且假设您比TypeScript更了解环境。
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;当不想写一个长长的类型代码,仅仅想让 TypeScript 知道某段特定的代码是没有问题的,any 类型是很有用的。
3. unknown
unknown 类型和 any 类型有些类似。一个 unknown 类型的变量可以再次赋值为任意其它类型,但一个 unknown 类型的变量只能赋值给另一个 any 与 unknown 类型的变量。
unknown 不能进行任何操作,需要进行类型缩小才行。
4. never
特点
- never 类型不携带任何的类型信息。
- never 类型被称为 Bottom Type,是整个类型系统层级中最底层的类型。
- 和 null、undefined 一样,它是所有类型的子类型,但只有 never 类型的变量能够赋值给另一个 never 类型变量。
使用例子
可以利用 never 类型变量仅能赋值给 never 类型变量的特性,来巧妙地进行分支处理检查:
declare const strOrNumOrBool: string | number | boolean;
if (typeof strOrNumOrBool === "string") {
// 一定是字符串!
strOrNumOrBool.charAt(1);
} else if (typeof strOrNumOrBool === "number") {
// 一定是 number 类型!
strOrNumOrBool.toFixed();
} else if (typeof strOrNumOrBool === "boolean") {
// 一定是 boolean 类型!
strOrNumOrBool === true;
} else {
// strOrNumOrBool 变成了 never 类型
const _exhaustiveCheck: never = strOrNumOrBool;
throw new Error(`Unknown input type: ${_exhaustiveCheck}`);
}三、复合类型
1. 数组
声明一个类似于 [1, 2, 3] 的数组类型,需要用到语法 number[]。这个语法可以适用于任何类型。也可能看到这种写法 Array<number>,是一样的。
let numbers: number[] = [1, 3, 5]
let numbers2: Array<number> = [1, 2, 3]ReadonlyArray 类型
ReadonlyArray 并不是一个可以用的构造器函数。需要这样使用:
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];更简短的写法 readonly Type[]。
function doStuff(values: readonly string[]) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
// ...but we can't mutate 'values'.
values.push("hello!"); // 属性 'push' 在类型 'readonly string[]' 上不存在。
}注意:Arrays 和 ReadonlyArray 并不能双向的赋值。也就是可读可写数组可以赋值给只读数组,但只读数组不能赋值给可读可写数组。
2. 元组
声明
元组类型是另一种类型的数组(或者说是数组的特例),它确切地知道包含多少个元素,以及特定索引对应的类型。
let position1: [number, number] = [29.1, 23.2];上述代码含义解释:
① 元组类型可以确切地标记出有多少个元素,以及每个元素的类型。
② 该示例中,元素有两个元素,每个元素的类型都是 number。
③ 该元组规定了只能有 2 个元素,再给第三个元素就会报错。
除了长度检查,简单的元组类型跟声明了 length 属性和具体的索引属性的 Array 是一样的。
interface StringNumberPair {
// specialized properties
length: 2;
0: string;
1: number;
// Other 'Array<string | number>' members...
slice(start?: number, end?: number): Array<string | number>;
}除了上面的声明方式,TS 还支持具名元组。
const arr7: [name: string, age: number, male?: boolean] = ['yxts', 599, true];可选属性
在元组类型中,也可以写一个可选属性,但可选元素必须在最后面,而且也会影响类型的 length。
type Either2dOr3d = [number, number, number?];
function setCoordinate(coord: Either2dOr3d) {
const [x, y, z] = coord; // const z: number | undefined
console.log(`Provided coordinates had ${coord.length} dimensions`); // (property) length: 2 | 3
}剩余元素语法
Tuples (元组) 也可以使用剩余元素语法,但必须是 array/tuple 类型:
type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];有剩余元素的元组并不会设置 length,因为它只知道在不同位置上的已知元素信息:
const a: StringNumberBooleans = ["hello", 1];
const b: StringNumberBooleans = ["beautiful", 2, true];
const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];
console.log(a.length); // (property) length: number
type StringNumberPair = [string, number];
const d: StringNumberPair = ['1', 1];
console.log(d.length); // (property) length: 2可选元素和剩余元素的存在,使得 TypeScript 可以在参数列表里使用元组,就像这样:
function readButtonInput(...args: [string, number, ...boolean[]]) {
const [name, version, ...input] = args;
// ...
}基本等同于:
function readButtonInput(name: string, version: number, ...input: boolean[]) {
// ...
}readonly 元组类型
元组类型也是可以设置 readonly。
function doSomething(pair: readonly [string, number]) {
pair[0] = "hello!"; // 不能赋值给 '0',因为它是一个只读属性。
}如果给一个 number[] 进行 const 断言,也会被推断为 readonly 字面量元组类型(readonly [3, 4])。
let point = [3, 4] as const; // 从 number[] 转变为了只读元组
function distanceFromOrigin([x, y]: [number, number]) {
return Math.sqrt(x ** 2 + y ** 2);
}
distanceFromOrigin(point);
// 类型 'readonly [3, 4]' 的参数不能赋值给类型 '[number, number]' 的参数。
// 类型 'readonly [3, 4]' 是 'readonly',不能赋值给可变的类型 '[number, number]'。尽管 distanceFromOrigin 并没有更改传入的元素,但函数希望传入一个可变元组。因为 point 的类型被推断为 readonly [3, 4],它跟 [number, number] 并不兼容,所以 TypeScript 给了一个报错。
3. 枚举
枚举的功能类似于字面量类型 + 联合类型组合的功能,也可以表示一组明确的可选值。
枚举:定义一组命名常量。它描述一个值,该值可以是这些命名常量中的一个。定义好枚举后,可以使用枚举名称作为一种类型。
关键字:enum + 枚举名称 + {枚举的可选值,逗号隔开}。
1)定义
enum Direciton { Up, Down, Left, Right };2)使用
function changeDirection1(direction: Direciton) {
console.log(direction);
}解释:
① 使用 enum 关键字定义枚举。
② 约定枚举名称、枚举中的值以大写字母开头。
③ 枚举中的多个值之间通过 ,(逗号)分隔。
④ 定义好枚举后,直接使用枚举名称作为类型注解。
注意:形参 direction 的类型为枚举 Direction,那么,实参的值就应该是枚举 Direction 成员的任意一个。
3)访问枚举成员
类似于 JS 中的对象,直接通过点(.)语法访问枚举的成员。
changeDirection1(Direciton.Down); // 枚举成员是有值的,默认为:从 0 开始自增的数值。4)枚举成员的值
问题:我们把枚举成员作为了函数的实参,既然可以把枚举成员作为函数的实参,那么它肯定是有值的,那么它的值是什么呢?
通过将鼠标移入 Direction.Up,可以看到枚举成员 Up 的值为 0。
注意:枚举成员是有值的,默认为:从 0 开始自增的数值。
数字枚举
把枚举成员的值为数字的枚举,称为数字枚举。
// Up = 10, Down = 11, Left = 12, Right = 13
enum Direciton { Up = 10, Down, Left, Right };
enum Direciton1 { Up = 2, Down = 4, Left = 6, Right = 8 };字符串枚举
字符串枚举是枚举成员的值是字符串。
enum Direciton1 { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT' };注意:字符串枚举没有自增长行为,因此,字符串枚举的每个成员必须有初始值。
5)原理
枚举是 TypeScript 添加的新特性,用于描述一个值可能是多个常量中的一个。不同于大部分的 TypeScript 特性,这并不是一个类型层面的增量,而是会添加到语言和运行时。例如:
enum Color {
Red,
Green,
Blue
}会被编译为以下 JavaScript 代码:
var Color;
(function (Color) {
Color[Color["Red"] = 0] = "Red";
Color[Color["Green"] = 1] = "Green";
Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));Color[Color["Red"] = 0] = "Red";:这行代码做了两件事。首先,它在 Color 对象中添加了一个名为 "Red" 的属性,其值为 0。然后,它又在 Color 对象中添加了一个名为 0 的属性,其值为 "Red"。这样,你就可以通过 Color.Red 访问 0,也可以通过 Color[0] 访问 "Red"。
6)建议
一般情况下,推荐使用字面量类型 + 联合类型组合的方式,因为相比枚举,这种方式更加直观、简洁、高效。(其他的类型会在编译为 JS 代码时自动移除。但是,枚举类型会被编译为 JS 代码!),因为枚举会被编译为 js,开销要比前者大。
4. 函数类型
1)指定参数、返回值的类型
[1] 单独指定
参数类型注解
当声明一个函数的时候,可以在每个参数后面添加一个类型注解,声明函数可以接受什么类型的参数。参数类型注解跟在参数名字后面:
// 参数类型注解
function greet(name: string) {
console.log("Hello, " + name.toUpperCase() + "!!");
}当参数有了类型注解的时候,TypeScript 便会检查函数的实参:
// 如果执行,将会是一个运行时错误
greet(42); // 类型“number”的参数不能赋值给类型“string”的参数即便对参数没有做类型注解,TypeScript 依然会检查传入参数的数量是否正确。
返回值类型注解
也可以添加返回值的类型注解。返回值的类型注解跟在参数列表后面:
function getFavoriteNumber(): number {
return 26;
}注意:即使没有标注,TypeScript 也会基于它的 return 语句推断函数的返回类型。
[2] 函数类型签名 / 函数类型表达式
解释:当函数作为表达式时,可以通过类似箭头函数形式的语法来为函数添加类型。
注意:这种形式只适用于函数表达式。
const add2: (num1: number, num2: number) => number = (num1, num2) => {
return num1 + num2
}详细解析一下这段代码:
const add2:定义了一个名为 add2 的常量。(num1: number, num2: number) => number:这是一个函数类型,表示函数接收两个 number 类型的参数,并返回一个 number 类型的结果。= (num1, num2) => { return num1 + num2 }:这是一个箭头函数,它接收两个参数 num1 和 num2,并返回它们的和。
上面代码的可读性非常差。因此,一般不推荐这么使用,要么直接在函数中进行参数和返回值的类型声明,要么使用类型别名将函数声明抽离出来:
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
// ...
}匿名函数有一点不同于函数声明,当 TypeScript 知道一个匿名函数将被怎样调用的时候,匿名函数的参数会被自动的指定类型。这个过程被称为上下文推断(contextual typing),因为正是从函数出现的上下文中推断出了它应该有的类型。
typescript// 虽然代码中没有类型注解,但是 TypeScript 仍然能够发现潜在的错误 const names = ["Alice", "Bob", "Eve"]; // 函数的上下文类型推断 names.forEach(function (s) { console.log(s.toUppercase()); // 类型 'string' 上不存在属性 'toUppercase'。你是不是想要 'toUpperCase'? }); // 上下文类型推断也适用于箭头函数 names.forEach((s) => { console.log(s.toUppercase()); // 类型 'string' 上不存在属性 'toUppercase'。你是不是想要 'toUpperCase'? });尽管参数 s 并没有添加类型注解,但 TypeScript 根据 forEach 函数的类型,以及传入的数组的类型,最后推断出了 s 的类型。
2)调用签名
调用签名是一种特殊的类型,它描述了一个函数的参数类型和返回值类型。你可以在一个对象类型中写一个调用签名,这样这个对象类型就同时包含了函数和属性。
例如,以下代码定义了一个带有属性的函数:
type MyFunctionWithProperties = {
(x: number, y: number): number; // 这是调用签名
description: string; // 这是一个属性
};
let func: MyFunctionWithProperties = function(x, y) { return x + y; };
func.description = "A function that adds two numbers";在这个例子中,MyFunctionWithProperties 是一个带有属性的函数类型,它有一个调用签名 (x: number, y: number): number 和一个属性 description。func 是一个符合这个类型的函数,你可以像调用普通函数一样调用它,也可以访问它的 description 属性。
3)构造签名
JavaScript 函数也可以使用 new 操作符调用,当被调用的时候,TypeScript 会认为这是一个构造函数 (constructors),因为他们会产生一个新对象。你可以写一个构造签名,方法是在调用签名前面加一个 new 关键词:
type SomeConstructor = {
new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
return new ctor("hello");
}一些对象,比如 Date 对象,可以直接调用,也可以使用 new 操作符调用,而你可以将调用签名和构造签名合并在一起:
interface CallOrConstruct {
new (s?: string): Date;
(n?: number): number;
}4)可选参数
使用函数实现某个功能时,参数可以传也可以不传。这种情况下,在给函数参数指定类型时,就用到了可选参数。
比如,数组的 slice 方法,可以 slice() 也可以 slice(1) 还可以 slice(1, 3) 。
function mySlice(num1?: number, num2?: number): void {
console.log('起始索引', num1, '结束索引', num2)
}
mySlice(1, 2)可选参数:在可传可不传的参数名称后面添加 ?(问号)。
注意:可选参数只能出现在参数列表的最后,也就是说可选参数后面不能再出现必选参数。
5. 对象类型
class、interface、普通的对象字面量。
JS 中的对象是由属性和方法构成的,而 TS 中对象的类型就是在描述对象的结构(有什么类型的属性和方法)。
1)定义
对象类型可以是匿名的
// 对象类型
let person: { name: string; age: number; sayHi(): void } = {
name: 'jack',
age: 25,
sayHi() {}
}解释:
① 直接使用 {} 来描述对象结构。属性采用属性名: 类型的形式;方法采用方法名(): 返回值类型的形式。
② 如果方法有参数,就在方法名后面的小括号中指定参数类型(比如:greet(name: string): void)。
③ 在一行代码中指定对象的多个属性类型时,使用 ;(分号)来分隔。如果一行代码只指定一个属性类型(通过换行来分隔多个属性类型),可以去掉 ;(分号)。也可以使用 , 分开属性,最后一个属性的分隔符加不加都行。
let person: {
name: string
age: number
sayHi(): void
} = {
name: 'jack',
age: 25,
sayHi() {}
}④ 每个属性对应的类型是可选的,如果你不指定,默认使用 any 类型。
⑤ 方法的类型也可以使用箭头函数形式(比如:{ sayHi: () => void })
let person: { name: string; age: number; sayHi: () => void } = {
name: 'jack',
age: 25,
sayHi() {}
}也可以使用接口进行定义
当一个对象类型被多次使用时,一般会使用接口(interface)来描述对象的类型,达到复用的目的。
interface Point {
x: number;
y: number; // 每一行只有一个属性类型,因此,属性类型后可以没有分号
}
function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
let point1: Point = {
x: 100,
y: 100
}
printCoord(point1);还可以通过类型别名定义。
type Person = {
name: string;
age: number;
};
function greet(person: Person) {
return "Hello " + person.name;
}类型别名和接口非常相似,大部分时候,可以任意选择使用。接口的几乎所有特性都可以在 type 中使用。
类型别名和接口的不同
类型别名可以给任意类型起别名,而接口只能声明对象。
类型别名不能多次声明,而接口可以多次声明。
类型别名不能继承,而接口可以继承。
自 TypeScript 2.7 起,可以通过交叉类型间接“扩展”类型别名,但这不是真正的扩展。
typescripttype Point = { x: number; y: number; }; type Point3D = Point & { z: number };类型别名不能被类实现,而接口可以。
2)属性修饰符
[1] 对象可选属性
在属性后面加个问号。
// 对象可选属性
function myAxios(config: {url: string; method?: string }) {
}
myAxios(
{
url: 'string'
}
)可选属性的语法与函数可选参数的语法一致,都使用 ?(问号)来表示。
在 JavaScript 中,如果你获取一个不存在的属性,你会得到一个 undefined 而不是一个运行时错误。因此,当你获取一个可选属性时,你需要在使用它前,先检查一下是否是 undefined。
function printName(obj: { first: string; last?: string }) {
// 错误 - 如果没有提供 'obj.last',可能会崩溃!
console.log(obj.last.toUpperCase());
// 对象可能是 'undefined'
if (obj.last !== undefined) {
// OK
console.log(obj.last.toUpperCase());
}
// 使用现代 JavaScript 语法的安全替代方案
console.log(obj.last?.toUpperCase());
}还可以使用对象解构赋值,在形参列表中。
typescriptfunction paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) { console.log("x coordinate at", xPos); // (parameter) xPos: number console.log("y coordinate at", yPos); // (parameter) yPos: number // ... }解构语法里没有放置类型注解的方式。
typescriptfunction draw({ shape: Shape, xPos: number = 100 /*...*/ }) { render(shape); // Cannot find name 'shape'. Did you mean 'Shape'? render(xPos); // Cannot find name 'xPos'. }在对象解构语法中,
shape: Shape表示的是把 shape 的值赋值给局部变量 Shape。xPos: number也是一样,会基于 xPos 创建一个名为 number 的变量。
[2] readonly 属性
一个标记为 readonly 的属性是不能被写入的(不会改变任何运行时的行为,主要是为了在编译时期提供类型检查)。但 readonly 仅仅表明属性本身是不能被重新写入的。
interface Home {
readonly resident: { name: string; age: number };
}
function visitForBirthday(home: Home) {
// 我们可以读取和更新 'home.resident' 的属性。
console.log(`Happy birthday ${home.resident.name}!`);
home.resident.age++;
}
function evict(home: Home) {
// 但是我们不能直接写入 'Home' 上的 'resident' 属性。
home.resident = {
// 不能赋值给 'resident',因为它是一个只读属性。
name: "Victor the Evictor",
age: 42,
};
}TypeScript 在检查两个类型是否兼容的时候,并不会考虑两个类型里的属性是否是 readonly。这就意味着,readonly 的值是可以通过别名修改的。
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
}
// works (正常运行)
let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'[3] 索引签名
不能提前知道一个类型里的所有属性的名字,但是知道这些值的特征。这种情况,就可以用一个索引签名来描述可能的值的类型。
interface StringArray {
// 如果尝试在具有 [index: string]: number; 索引签名的接口中添加函数属性,会导致类型错误,因为函数类型 () => number 并不是 number 类型的子类型。
[index: number]: string;
}
const myArray: StringArray = getStringArray();
const secondItem = myArray[1]; // const secondItem: string一个索引签名的属性类型必须是 string 或者是 number。虽然 TypeScript 可以同时支持 string 和 number 类型,但数字索引的返回类型一定要是字符索引返回类型的子类型。这是因为当使用一个数字进行索引的时候,JavaScript 实际上把它转成了一个字符串。
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// 错误:使用数字字符串进行索引可能会得到一个完全不同类型的 Animal!
interface NotOkay {
[x: number]: Animal; // 'number' 索引类型 'Animal' 不能赋值给 'string' 索引类型 'Dog'。
[x: string]: Dog;
}尽管字符串索引用来描述字典模式非常的有效,但也会强制要求所有的属性要匹配索引签名的返回类型。
interface NumberDictionary {
[index: string]: number;
length: number; // ok
name: string; // 类型为 'string' 的属性 'name' 不能赋值给 'string' 索引类型 'number'。
}然而,如果一个索引签名是属性类型的联合,那各种类型的属性就可以接受了:
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}最后,也可以设置索引签名为 readonly。
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory"; // 类型 'ReadonlyStringArray' 的索引签名只允许读取。因为索引签名是 readonly ,所以无法设置 myArray[2] 的值。
3)属性继承
如果两个接口之间有相同的属性或方法,可以将公共的属性或方法抽离出来,通过继承来实现复用。接口也可以继承多个类型。
例如:Point2D 和 Point3D,都有 x、y 两个属性,这样要重复写两次,可以这样做,但是很繁琐。
interface Point2D {
x: number
y: number
}
interface Point3D {
x: number
y: number
z: number
}更好的方式,接口继承。继承支持多个。
interface Point2D {
x: number
y: number
}
interface Point3D extends Point2D {
z: number
}解释:
① 使用 extends(继承)关键字实现了接口 Point3D 继承 Point2D。
② 继承后,Point3D 就有了 Point2D 的所有属性和方法(此时,Point3D 同时有 x、y、z 三个属性)。
接口继承的使用场景:有 2 个以上的接口有相同的属性,那么就可以将公共的属性或方法进行抽取到一个接口中,然后使用继承实现公共属性或方法的复用。
四、类
TypeScript 完全支持 ES2015 引入的 class 关键字。TypeScript 提供了类型注解和其他语法,允许你表达类与其他类型之间的关系。
1. 定义类
class Point {}2. 类成员
class Point {
// 字段 / 属性
x: number;
// 会阻止在构造函数之外的赋值
readonly y: number;
// 设置初始值后,不写类型注解,ts 也会自动推断类型的。z: number
z = 0;
y; // 类型注解是可选的,如果没有指定,会隐式的设置为 any
_size = 0;
// ************************************************************************************
// 类的构造函数:跟函数非常类似,可以使用带类型注解的参数、默认值、重载等。
/*
构造函数不能有类型参数。
构造函数不能有返回类型注解,因为总是返回类实例类型。
*/
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any = 0, y?: any = 0) {
// TBD
}
// ************************************************************************************
// 方法
scale(n: number): void {
this.x *= n;
this.y *= n;
}
// ************************************************************************************
// Getters / Setter 存取器
/*
如果 get 存在而 set 不存在,属性会被自动设置为 readonly。
如果 setter 参数的类型没有指定,它会被推断为 getter 的返回类型。
getters 和 setters 必须有相同的成员可见性。
从 TypeScript 4.3 起,存取器在读取和设置的时候可以使用不同的类型。
*/
// 注意这里返回的是 number 类型
get size(): number {
return this._size;
}
// 注意这里允许传入的是 string | number | boolean 类型
set size(value: string | number | boolean) {
let num = Number(value);
// 不允许 NaN、Infinity 等
if (!Number.isFinite(num)) {
this._size = 0;
}
this._size = num;
}
// ************************************************************************************
// 索引签名,和对象类型的索引签名是一样的。
/*
索引签名类型也是要捕获方法的类型。
*/
[s: string]: boolean | ((s: string) => boolean);
check(s: string) {
return this[s] as boolean;
}
// ************************************************************************************
// 静态成员
/*
静态成员同样可以使用 public protected 和 private 这些可见性修饰符。
静态成员也可以被继承。
静态成员不应该引用类的类型参数 (泛型) 。
覆写 Function 原型上的属性是不可以的。
*/
static f = 0;
static printX() {
console.log(Point.x);
}
// ************************************************************************************
// 类静态块
/*
静态块允许写一系列有自己作用域的语句,也可以获取类里的私有字段。
这意味着我们可以安心的写初始化代码:正常书写语句,无变量泄漏,还可以完全获取类中的属性和方法。
*/
static {
try {
const lastInstances = loadLastInstances();
Foo.#count += lastInstances.length;
} catch {}
}
}如果有一个基类,需要在使用任何
this.成员之前,要先在构造函数里调用 super()。忘记调用 super 是 JavaScript 中一个简单的错误,但是 TypeScript 会在需要的时候提醒你。typescriptclass Base { k = 4; } class Derived extends Base { constructor() { // 在 ES5 中打印错误的值; 在 ES6 中抛出异常。 console.log(this.k); // 在派生类的构造函数中访问 'this' 之前,必须调用 'super'。 super(); } }
3. 类继承与实现
1)implements
使用 implements 语句检查一个类是否满足一个特定的 interface。如果一个类没有正确的实现它,TypeScript 会报错。
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
class Ball implements Pingable {
// 'Ball' 类错误地实现了 'Pingable' 接口。
// 'Ball' 类型缺少 'Pingable' 类型所需的 'ping' 属性。
pong() {
console.log("pong!");
}
}小贴士:类也可以实现多个接口。
注意事项
implements 语句仅仅检查类是否按照接口类型实现,但它并不会改变类的类型或者方法的类型。一个常见的错误就是以为 implements 语句会改变类的类型 —— 然而实际上它并不会:
implements 关键字在 TypeScript 中主要用于强制类遵循特定的结构,而不是用于改变类的类型或者方法的类型。
interface Checkable {
check(name: string): boolean;
}
class NameChecker implements Checkable {
check(s) {
// 参数 's' 隐式具有 'any' 类型。
// 注意这里不会报错,因为是 any。
return s.toLowercse() === "ok"; // any
}
}在这个例子中,可能会以为 s 的类型会被 check 的 name: string 参数影响。实际上并没有,implements 语句并不会影响类的内部是如何检查或者类型推断的。因此,TypeScript 采用的是结构性类型系统。
类似的,实现一个有可选属性的接口,并不会创建这个属性:
interface A {
x: number;
y?: number;
}
class C implements A {
x = 0;
}
const c = new C();
c.y = 10; // 属性 'y' 不存在于类型 'C' 上。2)extends
类可以 extend 一个基类。一个派生类有基类所有的属性和方法,还可以定义额外的成员。
一个派生类可以覆写一个基类的字段或属性,也可以使用 super 语法访问基类的方法。
覆写属性
TypeScript 强制要求派生类总是它的基类的子类型,派生类需要遵循着它的基类的实现。具体来说,这个要求确保了以下几点:
① 方法和属性的兼容性:派生类可以覆写基类的方法和属性,但覆写后的方法和属性必须与基类中的原始版本兼容。例如,如果基类有一个返回 number 类型的方法,派生类覆写这个方法时也必须返回 number 类型。(参数类型、数量以及返回类型都需要匹配)
② 扩展而非缩减:派生类可以添加新的方法和属性,但不能删除继承自基类的方法和属性。这确保了派生类的对象至少具备基类的所有功能。
③ 构造函数和方法的参数:派生类中覆写的方法可以有与基类中相同的参数,或者有更宽松的参数(例如,参数是基类参数类型的子类型),但不能有更严格的参数类型。这是为了确保在使用基类类型引用派生类对象时,任何对基类方法的调用都能在派生类中正确工作。
下面是一个不合法的覆写属性。
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
// 使这个参数成为必需的
greet(name: string) {
// 类型 'Derived' 中的属性 'greet' 不能分配给基类型 'Base' 中的同名属性。
// 类型 '(name: string) => void' 不能分配给类型 '() => void'。
console.log(`Hello, ${name.toUpperCase()}`);
}
}即便我们忽视错误编译代码,这个例子也会运行错误。
const b: Base = new Derived();
// 因为 "name" 将是未定义的,所以会崩溃。
b.greet();解决方案是使用可选参数或者方法重载。
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
// 方法一: 使用可选参数
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
// 方法二: 方法重载签名
greet(): void;
greet(name: string): void;
}
const d = new Derived();
d.greet();
d.greet("reader");初始化顺序
类初始化的顺序,就像在 JavaScript 中定义的那样:
- 基类字段初始化
- 基类构造函数运行
- 派生类字段初始化
- 派生类构造函数运行
class Base {
name = "base";
constructor() {
console.log("My name is " + this.name);
}
}
class Derived extends Base {
name = "derived";
}
// 打印 "base",而不是 "derived"。
const d = new Derived();继承内置类型
问题
在 ES2015(也称为 ES6)中,当在子类的构造函数中调用 super() 时,它会调用父类的构造函数,并返回父类构造函数的结果。如果父类构造函数返回了一个对象,那么这个对象会替换子类构造函数中的 this。
typescriptclass Parent { constructor() { // 显式返回一个对象 return { name: "Parent Object" }; } } class Child extends Parent { yxts: string constructor() { super(); // 调用父类的构造函数 this.yxts = '666' // 通常情况下,这里的 this 会指向 Child 的实例 // 但由于父类返回了一个对象,这个 this 会被父类返回的对象替换 console.log(this); // 输出的将是 { name: "Parent Object", yxts: '666' },而不是 Child 的实例 } } const child = new Child(); // 由于父类构造函数返回了一个对象,这个对象替换了子类构造函数中的 this // 因此,child 实际上是父类构造函数返回的对象,而不是 Child 类的实例 console.log(child); // 输出结果 { name: "Parent Object", yxts: '666' }在 ES2015 中,内置对象(如 Error,Array 等)的构造函数会使用 new.target 来调整原型链。new.target 是一个在构造函数中可用的元属性,它引用的是正在构造的类。这使得内置对象的子类可以正确地继承父类的行为。
javascriptfunction Test() { console.log(new.target); } Test(); // 输出:undefined new Test(); // 输出:[Function: Test]但是,在 ES5 中,没有 new.target 这样的特性。因此,当使用 ES5 或者将 ES6 代码转换为 ES5 代码的时,内置对象的子类可能无法正确地继承原始类的行为。举例:
typescriptclass MsgError extends Error { constructor(m: string) { super(m); } sayHello() { return "hello " + this.message; } }1️⃣ 对象的方法可能是 undefined,所以调用 sayHello 会导致错误。
2️⃣ instanceof 失效,
(new MsgError()) instanceof MsgError会返回 false。这是因为 Error 构造函数内部大致是这样工作的:
typescriptfunction Error(message) { // 创建一个新对象 var instance = new Object(); // 设置消息等属性 instance.message = message; // 直接设置原型为 Error.prototype,忽略继承链 Object.setPrototypeOf(instance, Error.prototype); // 返回这个新对象,而不是使用 this return instance; }解决
手动的在 super(...) 调用后调整原型:
typescriptclass MsgError extends Error { constructor(m: string) { super(m); // 显式地设置原型 Object.setPrototypeOf(this, MsgError.prototype); } sayHello() { return "hello " + this.message; } }
4. 成员可见性
可以使用 TypeScript 控制某个方法或者属性是否对类以外的代码可见。
public:这是默认的可见性。公共成员在任何地方都可以访问。
protected:受保护的成员可以在类的内部和子类内部中访问,但不能使用类的实例调用。
受保护成员的公开
typescriptclass Base { protected m = 10; } class Derived extends Base { // 没有修饰符,所以默认为 '公开'。 m = 15; } const d = new Derived(); console.log(d.m); // OK交叉等级受保护成员访问
typescriptclass Base { protected x: number = 1; } class Derived1 extends Base { protected x: number = 5; } class Derived2 extends Base { f1(other: Derived2) { other.x = 10; } f2(other: Base) { other.x = 10; // 属性 'x' 是受保护的,只能通过 'Derived2' 类的实例访问。这是 'Base' 类的一个实例。 } }private:私有成员只能在类的内部访问。这意味着,不能从类的实例或子类中访问私有成员。
交叉实例私有成员的获取
typescriptclass A { private x = 10; public sameAs(other: A) { // 没问题 return other.x === this.x; } }
5. this
1)this 参数
在 TypeScript 方法或者函数的定义中,第一个参数且名字为 this 有特殊的含义。该参数会在编译的时候被抹除:
// TypeScript input with 'this' parameter
function fn(this: SomeType, x: number) {
/* ... */
}// JavaScript output
function fn(x) {
/* ... */
}TypeScript 会检查一个有 this 参数的函数在调用时是否有一个正确的上下文。可以给方法定义添加一个 this 参数,静态强制方法被正确调用:
class MyClass {
name = "MyClass";
getName(this: MyClass) {
return this.name;
}
}
const c = new MyClass();
// OK
c.getName();
// 错误,会导致崩溃
const g = c.getName;
console.log(g()); // 类型 'void' 的 'this' 上下文不能赋值给方法的 'MyClass' 类型的 'this'这个方法也有一些注意点,正好跟箭头函数相反:
JavaScript 调用者依然可能在没有意识到它的时候错误使用类方法。
即使在 TypeScript 中通过为方法定义添加一个特殊的 this 参数来静态强制正确的 this 上下文,一旦 TypeScript 代码被编译成 JavaScript,这种静态类型检查就不存在了。因此,JavaScript 代码的执行者可能会在不了解或不注意到正确的 this 上下文的情况下错误地使用这些方法。
每个类只有一个函数,而不是每一个类实例一个函数。
基类方法定义依然可以通过 super 调用。
2)this 类型
在类中,有一个特殊的名为 this 的类型,会动态的引用当前类的类型:
class Box {
contents: string = "";
set(value: string) { // (method) Box.set(value: string): this
this.contents = value;
return this;
}
}这里,TypeScript 推断 set 的返回类型为 this 而不是 Box。写一个 Box 的子类:
class ClearableBox extends Box {
clear() {
this.contents = "";
}
}
const a = new ClearableBox();
const b = a.set("hello"); // const b: ClearableBo也可以在参数类型注解中使用 this:
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}不同于写 other: Box,如果你有一个派生类,它的 sameAs 方法只接受来自同一个派生类的实例。
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
class DerivedBox extends Box {
otherContent: string = "?";
}
const base = new Box();
const derived = new DerivedBox();
derived.sameAs(base);
// 类型 'Box' 的参数不能赋值给类型 'DerivedBox' 的参数。
// 类型 'Box' 中缺少 'DerivedBox' 类型所需的 'otherContent' 属性。3)类型谓词 this
[1] 是什么
在 TypeScript 中,this is Type 是一种特殊的类型谓词,通常用在方法的返回类型位置。这种类型谓词表示方法的调用者(this)在方法返回后可以被视为 Type 类型。
这种类型谓词在运行时并没有实际效果,但在编译时,TypeScript 会根据这个类型谓词来进行类型检查和推断。
例如,可以在一个类的方法中使用 this is Type 来确保某个条件满足后,this 可以被视为 Type 类型:
typescriptclass MyClass { isType(): this is Type { // 在这里检查 this 是否可以被视为 Type 类型 // 如果可以,返回 true // 否则,返回 false } }如果 isType 方法返回 true,那么在调用 isType 方法后,this 可以被视为 Type 类型。这样,就可以在类型安全的情况下访问 Type 类型的属性和方法。
class FileSystemObject {
isFile(): this is FileRep {
return this instanceof FileRep;
}
isDirectory(): this is Directory {
return this instanceof Directory;
}
isNetworked(): this is Networked & this {
return this.networked;
}
constructor(public path: string, private networked: boolean) {}
}
class FileRep extends FileSystemObject {
constructor(path: string, public content: string) {
super(path, false);
}
}
class Directory extends FileSystemObject {
children: FileSystemObject[];
}
interface Networked {
host: string;
}
const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");
if (fso.isFile()) {
fso.content; // const fso: FileRep
} else if (fso.isDirectory()) {
fso.children; // const fso: Directory
} else if (fso.isNetworked()) {
fso.host; // const fso: Networked & FileSystemObject
}[2] 应用例子
可以在类和接口的方法返回的位置,使用 this is Type。当搭配使用类型收窄 (举个例子,if 语句) ,目标对象的类型会被收窄为更具体的 Type。
例子一:安全访问可选值
一个常见的基于 this 的类型保护的使用例子,会对一个特定的字段进行懒校验(lazy validation)。举个例子,在这个例子中,当 hasValue 被验证为 true 时,会从类型中移除 undefined。
class Box<T> {
value?: T;
hasValue(): this is { value: T } {
return this.value !== undefined;
}
}
const box = new Box();
box.value = "Gameboy";
box.value; // (property) Box<unknown>.value?: unknown
// 常见的不太优雅的写法
if (box.value !== undefined) {
doSomething(box.value);
}
// 使用类型保护的写法
if (box.hasValue()) {
box.value; // (property) value: unknown
}例子二:API 响应处理
class ApiResponse<T> {
data?: T;
error?: string;
isSuccess(): this is { data: T; error: undefined } {
return this.data !== undefined;
}
isError(): this is error: string } {
return this.error !== undefined;
}
}
async function handleUserData() {
const response = new ApiResponse<User>();
// ... 获取数据 ...
if (response.isSuccess()) {
// TypeScript 知道这里可以安全访问 response.data
updateUI(response.data);
} else if (response.isError()) {
// TypeScript 知道这里可以安全访问 response.error
showError(response.error);
}
}例子三:状态管理
class FormState<T> {
private data?: T;
private validationErrors?: string[];
isValid(): this is { data: T } {
return this.data !== undefined && !this.validationErrors?.length;
}
submit() {
if (this.isValid()) {
// 安全地访问 data,TypeScript 知道这里 data 一定存在
api.send(this.data);
}
}
}interface UserData {
name: string;
email: string;
}
class UserForm extends FormState<UserData> {
submit() {
if (this.isValid()) {
// TypeScript 知道这里的 this.data 一定存在
// 且是 UserData 类型
console.log(this.data.name); // 安全
console.log(this.data.email); // 安全
api.send(this.data); // 安全
} else {
// 在这个分支,TypeScript 知道 data 可能不存在
// console.log(this.data.name); // 类型错误!
}
}
}例子四:懒加载资源例子
// 定义一个泛型类,T 代表要加载的资源类型
class LazyResource<T> {
// resource 是可选的,初始为 undefined
private resource?: T;
// 类型保护方法:当返回 true 时,TypeScript 知道 resource 一定存在
isLoaded(): this is { resource: T } {
return this.resource !== undefined;
}
// 异步加载方法
async load() {
// 如果资源还没加载,则加载它
if (!this.isLoaded()) {
this.resource = await fetchResource();
}
// 返回已加载的资源
return this.resource;
}
// 使用资源的方法
use() {
if (this.isLoaded()) {
// 由于类型保护,TypeScript 知道这里 resource 一定存在
// 可以安全地调用 resource 上的方法
return this.resource.doSomething();
}
// 如果资源未加载,抛出错误
throw new Error('Resource not loaded');
}
}// 定义一个图片资源类型
interface ImageResource {
url: string;
width: number;
height: number;
doSomething(): void;
}
// 创建一个懒加载的图片资源
const lazyImage = new LazyResource<ImageResource>();
// 使用场景
async function handleImage() {
try {
// 首次使用时加载资源
await lazyImage.load();
// 使用已加载的资源
if (lazyImage.isLoaded()) {
lazyImage.use();
}
} catch (error) {
console.error('Failed to load image');
}
}
// 或者在 React 组件中使用
function ImageComponent() {
useEffect(() => {
lazyImage.load().catch(console.error);
}, []);
return (
<div>
{lazyImage.isLoaded() ? (
<img src={lazyImage.resource.url} />
) : (
<LoadingSpinner />
)}
</div>
);
}6. 参数属性
TypeScript 提供了特殊的语法,可以把一个构造函数参数转成一个同名同值的类属性。这些就被称为参数属性(parameter properties)。
可以通过在构造函数参数前添加一个可见性修饰符 public、private、protected 或者 readonly 来创建参数属性,最后这些类属性字段也会得到这些修饰符:
class Params {
constructor(
public readonly x: number,
protected y: number,
private z: number
) {
// 无需主体
}
}
const a = new Params(1, 2, 3);
console.log(a.x); // (property) Params.x: number
console.log(a.z); // 属性 'z' 是私有的,只能在 'Params' 类内部访问7. 类表达式
const someClass = class<Type> {
content: Type;
constructor(value: Type) {
this.content = value;
}
};
const m = new someClass("Hello, world"); // const m: someClass<string>8. 抽象类和成员
TypeScript 中,类、方法、字段都可以是抽象的(abstract)。
抽象方法或者抽象字段是不提供实现的。这些成员必须存在在一个抽象类中,这个抽象类也不能直接被实例化。
抽象类的作用是作为子类的基类,让子类实现所有的抽象成员。当一个类没有任何抽象成员,他就会被认为是具体的。
例子:
abstract class Base {
abstract getName(): string;
printName() {
console.log("Hello, " + this.getName());
}
}
const b = new Base(); // 无法创建抽象类的实例不能使用 new 实例 Base,因为它是抽象类。需要写一个派生类,并且实现抽象成员。
class Derived extends Base {
getName() {
return "world";
}
}
const d = new Derived();
d.printName();注意,如果忘记实现基类的抽象成员,会得到一个报错:
class Derived extends Base {
// 非抽象类 'Derived' 没有实现从 'Base' 类继承的抽象成员 'getName'。
// 忘记做任何事情。
}抽象构造签名
有的时候,希望接受传入可以继承一些抽象类产生一个类的实例的类构造函数。
举个例子,也许会写这样的代码:
function greet(ctor: typeof Base) {
const instance = new ctor();
// 无法创建抽象类的实例
instance.printName();
}TypeScript 会报错,告诉你正在尝试实例化一个抽象类。毕竟,根据 greet 的定义,这段代码应该是合法的:
// 糟糕!
greet(Base);但如果你写一个函数接受传入一个构造签名:
function greet(ctor: new () => Base) {
const instance = new ctor();
instance.printName();
}
greet(Derived);
// 类型 'typeof Base' 的参数不能赋值给类型 'new () => Base' 的参数。
// 不能将抽象构造函数类型赋值给非抽象构造函数类型。
greet(Base);现在 TypeScript 会正确的告诉你,哪一个类构造函数可以被调用,Derived 可以,因为它是具体的,而 Base 是不能的。
9. 类之间的关系
大部分时候,TypeScript 的类跟其他类型一样,会被结构性比较。
举个例子,这两个类可以用于替代彼此,因为它们结构是相等的:
class Point1 {
x = 0;
y = 0;
}
class Point2 {
x = 0;
y = 0;
}
// OK
const p: Point1 = new Point2();类似的还有,类的子类型之间可以建立关系,即使没有明显的继承:
class Person {
name: string;
age: number;
}
class Employee {
name: string;
age: number;
salary: number;
}
// OK
const p: Person = new Employee();这听起来有些简单,但还有一些例子可以看出奇怪的地方。
空类没有任何成员。在一个结构化类型系统中,没有成员的类型通常是任何其他类型的父类型。所以如果你写一个空类,任何东西都可以用来替换它:
class Empty {}
function fn(x: Empty) {
// 对 'x' 无法进行任何操作,所以我不会这么做
}
// All OK!
fn(window);
fn({});
fn(fn);五、高级类型
1. 联合类型
联合类型(Union Types)是一种复合类型,表示一个值可以是几种类型之一。
使用管道符(|)分隔每个类型。这意味着一个值可以是 type1 或 type2 或 type3 等。
let value: number | string;
value = 123; // OK
value = '123'; // OK
value = true; // 错误:类型“boolean”不可分配给类型“number | string”在使用中注意括号问题。例如:
添加小括号,表示首先 arr 是数组,其次数组元素是 number 或者是 string。
let arr: (number | string )[] = [1, 'a', 4, 'b', 4]另外一种情况,不添加括号,表示 arr 既可以是数字 number,也可以是字符串数组。
let arr: number | string[] = 122. 类型别名
1)声明
类型别名(Type Aliases)是一种创建新名称的方式,可以为任何类型创建一个新的名字,然后在代码中使用这个新的名字来代表原来的类型。
类型别名是通过 type 关键字来定义的。
type StringOrNumber = string | number;
let value: StringOrNumber;
value = 'hello'; // OK
value = 123; // OK2)工具类型
是什么
在类型别名中,类型别名可以这么声明自己能够接受的泛型(我称之为泛型坑位)。一旦接受了泛型,我们就叫它工具类型:
type Factory<T> = T | number | string;当然,我们一般不会直接使用工具类型来做类型标注,而是再度声明一个新的类型别名:
type FactoryWithBool = Factory<boolean>;
const foo: FactoryWithBool = true;有实际意义的工具类型
type MaybeNull<T> = T | null;
function process(input: MaybeNull<{ handler: () => {} }>) {
input?.handler();
}
// -------------------------------------------------------------
type MaybeArray<T> = T | T[];
function ensureArray<T>(input: MaybeArray<T>): T[] {
return Array.isArray(input) ? input : [input];
}3. 字面量类型
是什么
除了常见的类型 string 和 number,也可以将类型声明为更具体的数字或者字符串。
众所周知,在 JavaScript 中,有多种方式可以声明变量。比如 var 和 let,这种方式声明的变量后续可以被修改,还有 const,这种方式声明的变量则不能被修改,这就会影响 TypeScript 为字面量创建类型。
let changingString = "Hello World";
changingString = "Olá Mundo";
// 因为“changinString”可以表示任何可能的字符串,所以TypeScript在类型系统中就是这样描述它的
// let changingString: stringconst constantString = "Hello World";
// 由于“constantString”只能表示1个可能的字符串,因此它具有文字类型表示
// const constantString: "Hello World"字面量类型本身并没有什么太大用:
let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy"; // 类型“howdy”不可分配给类型“hello”。如果结合联合类型,就显得有用多了。举个例子,当函数只能传入一些固定的字符串时:
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre"); // 类型为“center”的参数不可分配给类型为“left”|“right”|“center””的参数数字字面量类型也是一样的:
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}当然了,也可以跟非字面量类型联合:
interface Options {
width: number;
}
function configure(x: Options | "auto") {
// ...
}
configure({ width: 100 });
configure("auto");
configure("automatic"); // 类型为“automatic”的参数不可分配给类型为“Options|”auto“”的参数还有一种字面量类型,布尔字面量。因为只有两种布尔字面量类型,true 和 false,类型 boolean 实际上就是联合类型 true | false 的别名。
字面量推断
declare function handleRequest(url: string, method: "GET" | "POST"): void;
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method); // 类型 'string' 的参数不能赋值给类型 '"GET" | "POST"' 的参数。在上面这个例子里,req.method 被推断为 string,而不是 "GET",因为在创建 req 和调用 handleRequest 函数之间,可能还有其他的代码,或许会将 req.method 赋值一个新字符串。所以 TypeScript 就报错了。
有两种方式可以解决。
添加一个类型断言改变推断结果:
typescript// Change 1: const req = { url: "https://example.com", method: "GET" as "GET" }; // Change 2 handleRequest(req.url, req.method as "GET");修改 1 表示“我有意让 req.method 的类型为字面量类型 "GET",这会阻止未来可能赋值为 "GUESS" 等字段。修改 2 表示“我知道 req.method 的值是 "GET"。
也可以使用 as const 把整个对象转为一个字面量类型:
typescript/* type const = { readonly url: "https://example.com"; readonly method: "GET"; } */ const req = { url: "https://example.com", method: "GET" } as const; handleRequest(req.url, req.method);as const 效果跟 const 类似,但是对类型系统而言,它可以确保所有的属性都被赋予一个字面量类型,而不是一个更通用的类型比如 string 或者 number。
4. 交叉类型
1)使用
[1] 对象类型交叉
TypeScript 也提供了名为交叉类型(Intersection types)的方法,用于合并已经存在的对象类型。
交叉类型的定义需要用到 & 操作符:
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;这里,我们连结 Colorful 和 Circle 产生了一个新的类型,新类型拥有 Colorful 和 Circle 的所有成员。
function draw(circle: Colorful & Circle) {
console.log(`Color was ${circle.color}`);
console.log(`Radius was ${circle.radius}`);
}
// okay
draw({ color: "blue", radius: 42 });
// oops
draw({ color: "red", raidus: 42 });
// “类型为 '{ color: string; raidus: number; }' 的参数不能赋值给 'Colorful & Circle' 类型的参数。对象字面量只能指定已知的属性,但 'raidus' 在 'Colorful & Circle' 类型中不存在。你是想写 'radius' 吗?”[2] 原始类型交叉
type StrAndNum = string & number; // never[3] 联合类型交叉
如果是两个联合类型组成的交叉类型呢?其实还是类似的思路,既然只需要实现一个联合类型成员就能认为是实现了这个联合类型,那么各实现两边联合类型中的一个就行了,也就是两边联合类型的交集:
type UnionIntersection1 = (1 | 2 | 3) & (1 | 2); // 1 | 2
type UnionIntersection2 = (string | number | symbol) & string; // string[4] 原始类型与对象类型交叉
2)接口继承与交叉类型对比
这两种方式在合并类型上看起来很相似,但实际上还是有很大的不同。最原则性的不同就是在于冲突怎么处理,这也是你决定选择哪种方式的主要原因。
interface Colorful {
color: string;
}
interface ColorfulSub extends Colorful {
color: number
}
// 接口 'ColorfulSub' 不正确地扩展了接口 'Colorful'。属性 'color' 的类型不兼容。类型 'number' 不能赋值给类型 'string'。使用继承的方式,如果重写类型会导致编译错误,但交叉类型不会:
interface Colorful {
color: string;
}
type ColorfulSub = Colorful & {
color: number
}虽然不会报错,但是 color 属性的类型是什么呢?答案是 never,取得是 string 和 number 的交集。

交叉类型通常会与映射类型结合。先举个简单的例子。
typescripttype bar = string & ("name" | "age" | 123 | true); // "name" | "age" /* string & ("name" | "age" | 123 | true) = (string & "name") | (string & "age") | (string & 123) | (string & true) = "name" | "age" | never | never = "name" | "age" 注意:一定要加 () , 否则结果是: string & "name" | "age" | 123 | true = (string & "name") | "age" | 123 | true = "name" | "age" | 123 | true 因为交叉类型 `&` 的优先级高于联合类型 `|` */相信你能看懂下面
${string & keyof Type}代码了。typescripttype PropEventSource<Type> = { on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void; };
六、泛型
软件工程的一个重要部分就是构建组件,组件不仅需要有定义良好和一致的 API,也需要是可复用的(reusable)。好的组件不仅能够兼容今天的数据类型,也能适用于未来可能出现的数据类型,这在构建大型软件系统时会给你最大的灵活度。
在比如 C# 和 Java 语言中,用来创建可复用组件的工具,我们称之为泛型(generics)。利用泛型,我们可以创建一个支持众多类型的组件,这让用户可以使用自己的类型消费(consume)这些组件。
1. 泛型定义
1)泛型函数
需要一种可以捕获参数类型的方式,然后再用它表示返回值的类型。这里我们用了一个类型变量(type variable),一种用在类型而非值上的特殊的变量。
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Type>(arg: Type) => Type = identity;有两种方式可以调用它。第一种方式是传入所有的参数,包括类型参数:
let output = identity<string>("myString"); // let output: string在这里,我们使用 <> 而不是 ()包裹了参数,并明确的设置 Type 为 string 作为函数调用的一个参数。
第二种方式可能更常见一些,这里我们使用了类型参数推断(type argument inference)(部分中文文档会翻译为“类型推论”),我们希望编译器能基于我们传入的参数自动推断和设置 Type 的值。
let output = identity("myString"); // let output: string注意:这次我们并没有用 <> 明确的传入类型,当编译器看到 myString 这个值,就会自动设置 Type 为它的类型(即 string)。
2)泛型接口
对象类型的调用签名的形式
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: { <Type>(arg: Type): Type } = identity;{ <Type>(arg: Type): Type } 是一个对象类型,它有一个调用签名。这个调用签名是一个泛型函数,它接受一个参数 arg,参数的类型是 Type,并返回一个相同类型的值。Type 是一个类型变量,它代表任何类型。
这意味着你可以将任何符合这个调用签名的函数赋值给 myIdentity 变量。例如:
myIdentity = function<Type>(arg: Type): Type {
return arg;
}在这个例子中,我们定义了一个泛型函数,并将这个函数赋值给 myIdentity。这个函数接受一个参数,并返回一个相同类型的值,所以它符合 { <Type>(arg: Type): Type } 的定义。
然后,你可以通过 myIdentity 变量来调用这个函数。例如:
let number = myIdentity(123);
let string = myIdentity('hello');在这个例子中,myIdentity(123) 调用了我们之前定义的函数,并传入了一个 number 类型的参数 123。myIdentity('hello') 调用了我们之前定义的函数,并传入了一个 string 类型的参数 'hello'。
泛型接口
让我们使用上个例子中的对象字面量,然后把它的代码移动到接口里:
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
// 使用
interface Person {
name: string;
age: number;
}
let person: Person = { name: "Bob", age: 25 };
let result6 = myIdentity<Person>(person);
console.log(result6); // 输出: { name: "Bob", age: 25 }有的时候,我们会希望将泛型参数作为整个接口的参数,这可以让我们清楚的知道传入的是什么参数 (举个例子:Dictionary<string> 而不是 Dictionary)。而且接口里其他的成员也可以看到。
// 泛型接口描述了一个函数
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
myIdentity(42); // 返回 42
myIdentity("Hello"); // 错误:参数类型不匹配3)泛型类
泛型类写法上类似于泛型接口。在类名后面,使用尖括号中 <> 包裹住类型参数列表:
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};2. 泛型约束
基本使用
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // 现在我们知道它有一个 .length 属性,所以不再报错。
return arg;
}现在这个泛型函数被约束了,它不再适用于所有类型:
loggingIdentity(3); // 类型 'number' 的参数不能赋值给类型 'Lengthwise' 的参数。需要传入符合约束条件的值:
loggingIdentity({ length: 10, value: 3 });在泛型约束中使用类型参数
希望获取一个对象给定属性名的值。为此,需要确保不会获取 obj 上不存在的属性。所以就要在两个类型之间建立一个约束:
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m"); // 类型为 'm' 的参数不能赋值给类型为 'a' | 'b' | 'c' | 'd' 的参数。在泛型中使用类类型
/* ------------------------------- 构造函数不支持带参数 ------------------------------- */
function create<Type>(c: { new (): Type }): Type {
return new c();
}
// 定义一个简单的类
class User {
name: string = "default";
sayHello() {
console.log(`Hello, I'm ${this.name}`);
}
}
// 使用 create 函数创建实例
const user = create(User);
user.sayHello(); // 输出: Hello, I'm default
/* ------------------------------- 构造函数支持带参数 ------------------------------- */
function createWithArgs<Type, Args extends any[]>(
c: { new (...args: Args): Type },
...args: Args
): Type {
return new c(...args);
}
// 使用示例
class WithParams {
constructor(public name: string) {}
}
const instance = createWithArgs(WithParams, "test");3. 泛型工具类型
Typescript 提供了一些工具类型来辅助进行常见的类型转换,这些类型全局可用。
1)Partial<Type>
将一个类型 Type 中的所有属性都变为可选的。
type MyType = {
a: number;
b: string;
};使用 Partial<MyType>,会得到一个新的类型:
type PartialMyType = Partial<MyType>; // { a?: number; b?: string; }Partial<Type> 源码:
type Partial<T> = {
[P in keyof T]?: T[P];
};2)Required<Type>
将一个类型 Type 中的所有属性都变为必需的。
type MyType = {
a?: number;
b?: string;
};使用 Required<Type>,会得到一个新的类型:
type RequiredMyType = Required<MyType>; // { a: number; b: string; }Required<Type> 源码:
type Required<T> = {
[P in keyof T]-?: T[P];
};3)Readonly<Type>
将一个类型 Type 中的所有属性都变为只读的。
type MyType = {
a: number;
b: string;
};使用 Readonly<MyType>,会得到一个新的类型:
type ReadonlyMyType = Readonly<MyType>; // { readonly a: number; readonly b: string; }Readonly<Type> 源码:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};4)Record<Keys, Type>
它可以创建一个类型,这个类型的属性名来自 Keys,属性值的类型为 Type。
例如,想创建一个类型,这个类型的属性名是 'a' 和 'b',属性值的类型是 number,可以使用 Record<'a' | 'b', number>:
type MyRecord = Record<'a' | 'b', number>; // { a: number; b: number; }Record<Keys, Type> 源码:
type Record<K extends keyof any, T> = {
[P in K]: T;
};5)Pick<Type, Keys>
从 Type 中挑选出一组属性 Keys,并创建一个新的类型。
type MyType = {
a: number;
b: string;
c: boolean;
};使用 Pick<Type, Keys>,会得到一个新的类型:
type PickedType = Pick<MyType, 'a' | 'b'>; // { a: number; b: string; }Pick<Type, Keys> 源码:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};6)Omit<Type, Keys>
与 Pick<Type, Keys> 相反。
/* -------------------------------------- 基础示例 -------------------------------------- */
interface User {
id: number;
name: string;
email: string;
password: string;
}
// 过滤多个属性
type BasicUser = Omit<User, 'password' | 'email'>;
// 结果:
// {
// id: number;
// name: string;
// }
/* -------------------------------------- 基础示例 -------------------------------------- */
// 排除所有可选属性或所有字符串类型的属性等
type ExcludeStrings<T> = {
[K in keyof T as T[K] extends string ? never : K]: T[K]
};
// 使用
type PersonWithoutStrings = ExcludeStrings<Person>;
// 结果类型
// {
// age: number;
// }在表达式 [K in keyof T as T[K] extends string ? never : K]: T[K] 中:
keyof T:遍历类型 T 的所有属性键。as T[K] extends string ? never : K:对于每一个键 K,检查 T 中对应的值的类型 T[K] 是否是字符串类型:如果是 (
T[K] extends string),则使用 never 作为该属性的键类型。在 TypeScript 中,never 类型用作键时,意味着这个属性会被排除。如果不是,就保持原来的键 K。
[K in ... as ...]: T[K]:根据上述条件,创建一个新的类型,其中包含的属性只有那些原类型中值不是字符串的属性。
Omit<Type, Keys> 源码:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// 如果每次都pick可能类型太多了
type HYOmit<T, K> = {
[P in keyof T as P extends K ? never : P]: T[P]
}7)Exclude<UnionType, ExcludedMembers>
可以从 UnionType 中排除出 ExcludedMembers,并创建一个新的类型。
例如,有一个联合类型:
type MyType = 'a' | 'b' | 'c';然后使用 Exclude<MyType, 'a' | 'b'>,会得到一个新的类型:
type ExcludedType = Exclude<MyType, 'a' | 'b'>; // 'c'Exclude<UnionType, ExcludedMembers> 源码:
type Exclude<T, U> = T extends U ? never : T;8)Extract<Type, Union>
可以从 Type 中提取出可以赋值给 Union 的类型。
例如,有一个联合类型:
type MyType = 'a' | 'b' | 'c'然后使用 Extract<MyType, 'a' | 'b'>,会得到一个新的类型:
type ExtractedType = Extract<MyType, 'a' | 'b'>; // 'a' | 'b'Extract<Type, Union> 源码:
type Extract<T, U> = T extends U ? T : never;9)NonNullable<Type>
用于构造一个类型,这个类型从 Type 中排除了所有的 null 、 undefined 的类型。
// 构造一个类型,排除 `null` 和 `undefined`。
type NonNullable<T> = T extends undefined | null ? never : T;
type unionType = string | number | undefined | null;
type unionType2 = NonNullable<unionType>;10)
11)InstanceType<T>
用于获取构造函数类型 T 的实例类型。换句话说,它返回由构造函数 T 创建的对象类型。
语法
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;详细解释
T extends new (...args: any) => any:这是一个泛型约束,确保T是一个构造函数类型。T extends new (...args: any) => infer R ? R : any:这是条件类型,检查T是否是一个构造函数类型。- 如果是,则使用
infer关键字推断构造函数的返回类型R。 - 如果不是,则返回
any类型。
- 如果是,则使用
示例
以下是一些示例,展示如何使用 InstanceType 工具类型:
示例 1:简单类
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
type PersonInstance = InstanceType<typeof Person>;
// PersonInstance 的类型为 Person
const person: PersonInstance = new Person("Alice");
console.log(person.name); // 输出: Alice在这个示例中:
typeof Person是构造函数类型new (name: string) => Person。InstanceType<typeof Person>返回Person类型。
示例 2:工厂函数返回类
function createPersonClass() {
return class {
name: string;
constructor(name: string) {
this.name = name;
}
};
}
const PersonClass = createPersonClass();
type PersonInstance = InstanceType<typeof PersonClass>;
// PersonInstance 的类型为 createPersonClass 返回的类的实例类型
const person: PersonInstance = new PersonClass("Bob");
console.log(person.name); // 输出: Bob在这个示例中:
typeof PersonClass是工厂函数返回的类的构造函数类型。InstanceType<typeof PersonClass>返回该类的实例类型。
使用场景
InstanceType 工具类型在以下场景中非常有用:
- 类型推断:从构造函数类型推断实例类型。
- 泛型编程:在泛型函数或类中使用构造函数类型时,确保类型安全。
- 类型转换:将构造函数类型转换为实例类型,以便在类型注解中使用。
总结来说,InstanceType 是一个强大的工具类型,用于处理和操作构造函数及其实例类型,增强了 TypeScript 的类型系统的灵活性和表达能力。
七、类型断言
在 TypeScript 中,类型断言是一种方式,它允许你告诉编译器你比它更了解某个值的类型。类型断言并不会改变数据的实际类型,它只是在编译时期提供类型检查。
在 TypeScript 中,有两种语法可以进行类型断言:
尖括号语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;因为 someValue 是 any 类型,所以 TypeScript 不知道它有 length 属性,就需要使用类型断言来告诉 TypeScript,someValue 是 string 类型。
注意:在 TypeScript 中使用 JSX 时,只有 as 语法断言是被允许的。
疑问:怎么知道 a 标签的类型是 HTMLAnchorElement?怎么知道 div 的类型是什么,span 的类型又是什么?
技巧:选中那个标签,在浏览器控制台,通过 console.dir($0) 打印 DOM 元素,在属性列表的最后面,即可看到该元素的类型。
TypeScript 仅仅允许类型断言转换为一个更加具体或者更不具体的类型。
const x = "hello" as number;
// 将类型 'string' 转换为类型 'number' 可能是一个错误,因为这两种类型之间没有足够的重叠。如果这是有意为之,那么首先应将表达式转换为 'unknown' 类型。可以使用双重断言,先断言为 any(或者是 unknown),然后再断言为期望的类型:
const a = (expr as any) as T;非空断言操作符(后缀 !)(Non-null Assertion Operator)
非空断言操作符 ! 是一种类型断言,它告诉 TypeScript 编译器,你确定这个表达式的值不会是 null 或 undefined。这样,TypeScript 就不会对这个表达式进行 null 或 undefined 的检查。
function liveDangerously(x?: number | null) {
// No error
console.log(x!.toFixed());
}就像其他的类型断言,这也不会更改任何运行时的行为。只有当你明确的知道这个值不可能是 null 或者 undefined 时才使用!
八、类型操作符
1. 索引类型查询 / keyof 类型操作符
1)使用
对一个对象类型使用 keyof 操作符,会返回该对象属性名组成的一个字符串或者数字字面量的联合。
例如,考虑以下的对象类型:
type MyObject = {
1: string;
2: number;
3: boolean;
};这个例子中,MyObject 的所有键都是数字。如果我们对 MyObject 使用 keyof 操作符,我们将得到数字字面量的联合类型 1 | 2 | 3:
type MyKeys = keyof MyObject; // 1 | 2 | 3但如果这个类型有一个 string 或者 number 类型的索引签名,keyof 则会直接返回这些类型:
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish; // type A = number
type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // type M = string | number注意:在这个例子中,M 是 string | number,这是因为 JavaScript 对象的属性名会被强制转为一个字符串,所以 obj[0] 和 obj["0"] 是一样的。
数字字面量联合类型
keyof 也可能返回一个数字字面量的联合类型,那什么时候会返回数字字面量联合类型呢,我们可以尝试构建这样一个对象:
const NumericObject = {
[1]: "雨下田上一号",
[2]: "雨下田上二号",
[3]: "雨下田上三号"
};
type result = keyof typeof NumericObject
// typeof NumbericObject 的结果为:
// {
// 1: string;
// 2: string;
// 3: string;
// }
// 所以最终的结果为:
// type result = 1 | 2 | 3Symbol
其实 TypeScript 也可以支持 symbol 类型的属性名:
由于 symbolToNumberMap 的键是 Symbol 类型的变量,而 Symbol 是一种独特且不可变的原始数据类型,因此 KS 的类型将是这三个 Symbol 类型变量的联合类型。
const sym1 = Symbol();
const sym2 = Symbol();
const sym3 = Symbol();
const symbolToNumberMap = {
[sym1]: 1,
[sym2]: 2,
[sym3]: 3,
};
type KS = keyof typeof symbolToNumberMap; // typeof sym1 | typeof sym2 | typeof sym3这也就是为什么当我们在泛型中像下面的例子中使用,会如此报错:
function useKey<T, K extends keyof T>(o: T, k: K) {
var name: string = k; // 类型 'string | number | symbol' 不能赋值给类型 'string'。
}如果你确定只使用字符串类型的属性名,你可以这样写:
function useKey<T, K extends Extract<keyof T, string>>(o: T, k: K) {
var name: string = k; // OK
}而如果你要处理所有的属性名,你可以这样写:
function useKey<T, K extends keyof T>(o: T, k: K) {
var name: string | number | symbol = k;
}类和接口
对类使用 keyof:
// 例子一
class Person {
name: "雨下田上"
}
type result = keyof Person; // type result = "name"// 例子二
class Person {
[1]: string = "雨下田上";
}
type result = keyof Person; // type result = 1对接口使用 keyof:
interface Person {
name: "string";
}
type result = keyof Person; // type result = "name"2)实战
希望获取一个对象给定属性名的值,为此,需要确保不会获取 obj 上不存在的属性。所以在两个类型之间建立一个约束:
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m"); // 类型为 'm' 的参数不能赋值给类型为 'a' | 'b' | 'c' | 'd' 的参数。2. typeof 类型操作符
1)介绍
JavaScript 本身就有 typeof 操作符,可以在表达式上下文中(expression context)使用:
console.log(typeof "Hello world"); // Prints "string"而 TypeScript 添加的 typeof 方法可以在类型上下文(type context)中使用,用于获取一个变量或者属性的类型。
let s = "hello";
let n: typeof s; // let n: string如果仅仅用来判断基本的类型,自然是没什么太大用,和其他的类型操作符搭配使用才能发挥它的作用。
举个例子:比如搭配 TypeScript 内置的 ReturnType<T>。你传入一个函数类型,ReturnType<T> 会返回该函数的返回值的类型:
type Predicate = (x: unknown) => boolean;
type K = ReturnType<Predicate>; // type K = boolean如果我们直接对一个函数名使用 ReturnType,会看到这样一个报错:
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<f>; // “f”指的是一个值,但在此处用作类型。你的意思是“typeof f”吗?这是因为值(values)和类型(types)并不是一种东西。为了获取函数 f 的类型,就需要使用 typeof:
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>;
// typeof f 的结果:
// () => {
// x: number;
// y: number;
// }
// ReturnType<typeof f> 的结果:
// type P = {
// x: number;
// y: number;
// }TypeScript 有意的限制了可以使用 typeof 的表达式的种类。
在 TypeScript 中,只有对标识符(比如变量名)或者他们的属性使用 typeof 才是合法的。这可能会导致一些令人迷惑的问题:
// Meant to use = ReturnType<typeof msgbox>
let shouldContinue: typeof msgbox("Are you sure you want to continue?");
// 预期的 ','我们本意是想获取 msgbox("Are you sure you want to continue?") 的返回值的类型,所以直接使用了 typeof msgbox("Are you sure you want to continue?"),看似能正常执行,但实际并不会,这是因为 typeof 只能对标识符和属性使用。而正确的写法应该是:
ReturnType<typeof msgbox>大白话就是只可以对已知的东西(标识符与属性)进行 typeof。函数运行是动态的。TypeScript 是要在编译时确定。
2)使用
[1] 对对象使用
const person = { name: "kevin", age: "18" }
type Kevin = typeof person;
// type Kevin = {
// name: string;
// age: string;
// }[2] 对函数类型使用
function identity<Type>(arg: Type): Type {
return arg;
}
type result = typeof identity;
// type result = <Type>(arg: Type) => Type[3] 对 enum 使用
在 TypeScript 中,enum 是一种新的数据类型,但在具体运行的时候,它会被编译成对象。
enum UserResponse {
No = 0,
Yes = 1,
}对应编译的 JavaScript 代码为:
var UserResponse;
(function (UserResponse) {
UserResponse[UserResponse["No"] = 0] = "No";
UserResponse[UserResponse["Yes"] = 1] = "Yes";
})(UserResponse || (UserResponse = {}));如果我们打印一下 UserResponse:
console.log(UserResponse);
// [LOG]: {
// "0": "No",
// "1": "Yes",
// "No": 0,
// "Yes": 1
// }而如果对 UserResponse 使用 typeof:
type result = typeof UserResponse;
// result 类型类似于:
// {
// "No": number,
// "YES": number
// }
// ok
const a: result = {
"No": 2,
"Yes": 3
}不过对一个 enum 类型只使用 typeof 一般没什么用,通常还会搭配 keyof 操作符用于获取属性名的联合字符串:
type result = keyof typeof UserResponse;
// type result = "No" | "Yes"九、索引访问类型 / 索引类型访问
1. 使用
例一
使用索引访问类型查找另外一个类型上的特定属性:
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // type Age = number因为索引名本身就是一个类型,所以也可以使用联合、keyof 或者其他类型:
type I1 = Person["age" | "name"]; // type I1 = string | number
type I2 = Person[keyof Person]; // type I2 = string | number | boolean
type AliveOrName = "alive" | "name";
type I3 = Person[AliveOrName]; // type I3 = string | boolean注意:查找一个不存在的属性,TypeScript 会报错。
例二
使用 number 来获取数组元素的类型。结合 typeof 可以方便的捕获数组字面量的元素类型:
const MyArray = [
{ name: "Alice", age: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 },
];
type Person = typeof MyArray[number];
// type Person = {
// name: string;
// age: number;
// }
type Age = typeof MyArray[number]["age"]; // type Age = number
// Or
type Age2 = Person["age"]; // type Age2 = number2. 实战
有这样一个业务场景,一个页面要用在不同的 APP 里,比如淘宝、天猫、支付宝,根据所在 APP 的不同,调用的底层 API 会不同。
type app = 'TaoBao' | 'Tmall' | 'Alipay';
function getPhoto(app: app) {
// ...
}
getPhoto('TaoBao'); // ok
getPhoto('whatever'); // not ok结合 typeof 和本节的内容实现:
const APP = ['TaoBao', 'Tmall', 'Alipay'] as const; // 从 string[] 变为只读的元组
type app = typeof APP[number]; // type app = "TaoBao" | "Tmall" | "Alipay"
function getPhoto(app: app) {
// ...
}
getPhoto('TaoBao'); // ok
getPhoto('whatever'); // not ok来一步步解析:
as const 作用是什么?as const 将字符串数组变为 readonly 的元组类型。
type app = typeof APP[number]; 代码可以改为 type app = typeof APP; 吗?不可以。
type typeOfAPP = typeof APP; // type typeOfAPP = readonly ["TaoBao", "Tmall", "Alipay"]十、条件类型
1.基本使用
帮助我们描述输入类型和输出类型之间的关系。
条件类型的写法有点类似于 JavaScript 中的条件表达式(condition ? trueExpression : falseExpression):
SomeType extends OtherType ? TrueType : FalseType;条件类型可以帮助我们减少重载签名。例如:一个函数接受 2 个类型的参数。
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
}如果一个库这样写,就会变得非常笨重。如果增加一种新的类型,重载的数量将呈指数增加。其实完全可以把逻辑写在条件类型中:
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;使用这个条件类型,可以简化掉函数重载:
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
let a = createLabel("typescript"); // let a: NameLabel
let b = createLabel(2.8); // let b: IdLabel
let c = createLabel(Math.random() ? "hello" : 42); // let c: NameLabel | IdLabel2. 在条件类型里推断
当在 extends 子句中使用 infer 关键字时,实际上是在声明一个类型变量,这个类型变量将会持有被推断出的类型。然后,可以在条件类型的 true 分支(即 ? 后面的部分)中使用这个类型变量。
例如,下面的 Unpacked 类型可以用来获取一个数组的元素类型:
type Unpacked<T> = T extends (infer U)[] ? U : T;3. 分发条件类型
当在泛型中使用条件类型的时候,如果传入一个联合类型,就会变成分发的。
type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>; // type StrArrOrNumArr = string[] | number[]通常这是我们期望的行为,如果要避免这种行为,可以用方括号包裹 extends 关键字的每一部分。
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
// 'StrArrOrNumArr' 不再是一个联合类型。
type StrArrOrNumArr = ToArrayNonDist<string | number>; // type StrArrOrNumArr = (string | number)[]十一、映射类型
1. 基本使用
一个类型需要基于另外一个类型,但是又不想拷贝一份,这个时候可以考虑使用映射类型。
映射类型,就是使用了 PropertyKeys 联合类型的泛型,其中 PropertyKeys 多是通过 keyof 创建,然后循环遍历键名创建一个类型:
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};在这个例子中,OptionsFlags 会遍历 Type 所有的属性,然后设置为布尔类型。
type FeatureFlags = {
darkMode: () => void;
newUserProfile: () => void;
};
type FeatureOptions = OptionsFlags<FeatureFlags>;
// type FeatureOptions = {
// darkMode: boolean;
// newUserProfile: boolean;
// }2. 映射修饰符
在使用映射类型时,有两个额外的修饰符可能会用到,一个是 readonly,用于设置属性只读,一个是 ?,用于设置属性可选。
可以通过前缀 - 或者 + 删除或者添加这些修饰符,如果没有写前缀,相当于使用了 + 前缀。
// 删除属性中的只读属性
type CreateMutable<Type> = {
-readonly [Property in keyof Type]: Type[Property];
};
type LockedAccount = {
readonly id: string;
readonly name: string;
};
type UnlockedAccount = CreateMutable<LockedAccount>;
// type UnlockedAccount = {
// id: string;
// name: string;
// }// 删除属性中的可选属性
type Concrete<Type> = {
[Property in keyof Type]-?: Type[Property];
};
type MaybeUser = {
id: string;
name?: string;
age?: number;
};
type User = Concrete<MaybeUser>;
// type User = {
// id: string;
// name: string;
// age: number;
// }3. 通过 as 实现键名重新映射
在 TypeScript 4.1 及以后,可以在映射类型中使用 as 语句实现键名重新映射:
type MappedTypeWithNewProperties<Type> = {
[Properties in keyof Type as NewKeyType]: Type[Properties]
}使用
type NewKeyType = `new_${string}`;
type OriginalType = {
foo: number;
bar: string;
};
type NewType = MappedTypeWithNewProperties<OriginalType>;
// 结果类似于:
// {
// new_foo: number;
// new_bar: string;
// }十二、模板字面量类型
1. 基本使用
模板字面量类型以字符串字面量类型为基础,可以通过联合类型扩展成多个字符串。它们跟 JavaScript 的模板字符串是相同的语法,但是只能用在类型操作中。
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"类型中的字符串联合类型
type PropEventSource<Type> = {
on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};
// 创建一个带有 'on' 方法的 '被观察对象',以便你可以监视属性的变化。
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;模板字面量的推断
type PropEventSource<Type> = {
on<Key extends string & keyof Type>
(eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void ): void;
};
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
person.on("firstNameChanged", newName => {
// (parameter) newName: string
console.log(`new name is ${newName.toUpperCase()}`);
});
person.on("ageChanged", newAge => {
// (parameter) newAge: number
if (newAge < 0) {
console.warn("warning! negative age");
}
})再看一个例子。
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};
interface Person {
name: string;
age: number;
599: string;
}
type PersonGetters = Getters<Person>;
// 等同于
// {
// getName: () => string;
// getAge: () => number;
// }2. 内置字符操作类型
TypeScript 的一些类型可以用于字符操作,这些类型考虑到性能,被内置在编译器中,你不能在 .d.ts 文件里找到它们。
1)Uppercase
把每个字符转为大写形式:
type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting> // type ShoutyGreeting = "HELLO, WORLD"
type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
type MainID = ASCIICacheKey<"my_app"> // type MainID = "ID-MY_APP"2)Lowercase
把每个字符转为小写形式:
type Greeting = "Hello, world"
type QuietGreeting = Lowercase<Greeting> // type QuietGreeting = "hello, world"
type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`
type MainID = ASCIICacheKey<"MY_APP"> // type MainID = "id-my_app"3)Capitalize
把字符串的第一个字符转为大写形式:
type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>; // type Greeting = "Hello, world"4)Uncapitalize
把字符串的第一个字符转换为小写形式:
type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>; // type UncomfortableGreeting = "hELLO WORLD"第三章:模块
一、TypeScript 具体的 ES 模块语法
1. 基本语法
跟 JavaScript 一样,使用相同的语法来导出和导入。
// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export interface Dog {
breeds: string[];
yearOfBirth: number;
}
// @filename: app.ts
import { Cat, Dog } from "./animal.ts";
type Animals = Cat | Dog;2. 推荐的语法
TypeScript 已经在两个方面拓展了 import 语法,方便类型导入。
1)导入类型
// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export type Dog = { breeds: string[]; yearOfBirth: number };
export const createCatName = () => "fluffy";
// @filename: valid.ts
import type { Cat, Dog } from "./animal.ts";
export type Animals = Cat | Dog;
// @filename: app.ts
import type { createCatName } from "./animal.ts";
const name = createCatName(); // “createCatName” 不能用作值,因为它是使用 “import type” 导入的。2)内置类型导入
TypeScript 4.5 也允许单独的导入,你需要使用 type 前缀 ,表明被导入的是一个类型:
// @filename: app.ts
import { createCatName, type Cat, type Dog } from "./animal.js";
export type Animals = Cat | Dog;
const name = createCatName();这些可以让一个非 TypeScript 编译器比如 Babel、swc 或者 esbuild 知道什么样的导入可以被安全移除。
二、declare 关键字
declare 关键字用来告诉编译器,某个类型是存在的,可以在当前文件中使用。
它的主要作用,就是让当前文件可以使用其他文件声明的类型。举例来说,自己的脚本使用外部库定义的函数,编译器会因为不知道外部函数的类型定义而报错,这时就可以在自己的脚本里面使用 declare 关键字,告诉编译器外部函数的类型。这样的话,编译单个脚本就不会因为使用了外部类型而报错。
declare 关键字的重要特点是,它只是通知编译器某个类型是存在的,不用给出具体实现。比如,只描述函数的类型,不给出函数的实现,如果不使用 declare,这是做不到的。
declare 只能用来描述已经存在的变量和数据结构,不能用来声明新的变量和数据结构。另外,所有 declare 语句都不会出现在编译后的文件里面。
三、.d.ts 文件
1. 是什么?
TypeScript 中的 .d.ts 文件是类型声明文件,它们用来为 TypeScript 提供有关 JavaScript 代码结构的类型信息。
2. 加载机制
在 TypeScript 中,.d.ts 文件的引入机制有以下几种情况。
1)同名引入
对于你自己写的模块,TypeScript 会默认引入与该文件同名的类型声明文件。例如,如果你有一个 foo.js 文件和一个同目录下的 foo.d.ts 文件,当你在 TypeScript 中 import 该模块时,类型信息将由 foo.d.ts 提供。
2)自动引入
对于 npm 安装的第三方库,如 lodash,如果你安装了对应的类型声明(例如通过 npm install @types/lodash),TypeScript 会自动识别这些 node_modules/@types 的声明文件,无需显式引入它们。
但也可以通过配置更改。
- typeRoots 设置类型模块所在的目录,默认是
node_modules/@types - 该目录里面的模块会自动加入编译。一旦指定了该属性,就不会再用默认值
node_modules/@types里面的类型模块。 - 该属性的值是一个数组,数组的每个成员就是一个目录,它们的路径是相对于
tsconfig.json位置。
{
"compilerOptions": {
"typeRoots": ["./typings", "./vendor/types"]
}
}3)通过 tsconfig.json 配置自动加载
include 属性是用来指明 TypeScript 编译器应该包含哪些文件的。
{
"include": [
"src/**/*" // `src/**/*` 表示包含 src 目录下的所有文件和子目录中的所有文件,无论文件的扩展名是什么,都应该纳入 TypeScript 编译的范围
]
}解释:
src/*:这是一个 glob 模式,其中:
- src/ 表示源代码位于项目根目录下的 src 目录中。
**表示任意数量的子目录(包括零个),这是一个通配符。*表示任意数量的字符(不含路径分隔符),也是一个通配符。
如果 tsconfig.json 不做任何特殊设置,默认会加载所有的 .d.ts 文件,包括根目录下和任何文件夹内。
3. 外部定义类型声明
1)第三方库
第一:很多第三方库默认都自带类型声明文件,这种就不用我们管了。
第二:如果没有自带,ts 社区基本上也提供了他们的类型文件。TypeScript 社区主要使用 DefinitelyTyped 仓库,各种类型声明文件都会提交到那里,已经包含了几千个第三方库。
这些声明文件都会作为一个单独的库,发布到 npm 的 @types 名称空间之下。比如,jQuery 的类型声明文件就发布成 @types/jquery 这个库,使用时安装这个库就可以了。
npm install @types/jquery --save-dev执行上面的命令,@types/jquery 这个库就安装到项目的 node_modules/@types/jquery 目录,里面的 index.d.ts 文件就是 jQuery 的类型声明文件。
2)自定义声明
declare module
declare module 用于声明外部模块的类型。在使用第三方模块(比如某个 npm 包)时,你可以使用这种方法来定义模块的类型声明。
declare module 用来定义模块的类型声明,并且这种声明是可以被导入的。这通常适用于文件模块。在 ES6 模块系统中,任何包含顶层 import 或 export 的文件都被视为一个模块。
ts 中 declare module 这个 module 指的是哪种文件?
在 TypeScript 中,declare module 是用来声明外部模块的。这个"模块"指的是通过 import/require 等方式引入的文件或者库。这可能是一个 npm 库,如 lodash 或 jQuery,或者是你的项目中的另一个 JavaScript/TypeScript 文件。
这个 declare module 'jquery' 里的 'jquery' 就是所谓的"模块"。这里是一个字符串,可以写任何你希望的模块名。这个字符串和你代码中 import/require 的模块名全部匹配,如你写 import $ from 'jquery',那么 TypeScript 就会去找 declare module 'jquery' 里有没有对应的类型声明。
三斜线指令
你可以在文件顶部添加如下指令来显式告诉 TypeScript 引入特定的 .d.ts 文件:
/// <reference path="my-declaration-file.d.ts" />
/// <reference path="my-declaration-file.d.ts" />这种方式并不推荐用于模块化的代码,适用于全局脚本的场景。
全局变量
全局变量的声明文件主要有以下几种语法:
- declare var 声明全局变量
- declare function 声明全局方法
- declare class 声明全局类
- declare enum 声明全局枚举类型
- declare namespace 声明(含有子属性的)全局对象
- interface 和 type 声明全局类型
附加
一、类型系统层级
never < 字面量类型 < 包含此字面量类型的联合类型(同一基础类型) < 对应的原始类型 < 包含此原始类型的联合类型。
原始类型 < 原始类型对应的装箱类型 < Object 类型 < any / unknown。
基本类型:
number
string
boolean
undefined
null
symbol
bigint
对象类型:
object
array
tuple
enum
interface
class
函数类型:
Function
具体的函数签名
字面量类型:
字符串字面量类型
数字字面量类型
布尔字面量类型
联合和交叉类型:
联合类型(Union Types):Type1 | Type2
交叉类型(Intersection Types):Type1 & Type2
特殊类型:
any
unknown
never
void
泛型:
泛型函数
泛型接口
泛型类
条件类型:
T extends U ? X : Y
实用工具类型:
Partial<T>
Required<T>
Readonly<T>
Record<K, T>
Pick<T, K>
Omit<T, K>
Exclude<T, U>
Extract<T, U>
NonNullable<T>
ReturnType<T>
InstanceType<T>
Parameters<T>
ConstructorParameters<T>
ThisParameterType<T>
OmitThisParameter<T>
ThisType<T>二、ts 封装 axios 库
封装的 axios 需要满足如下功能:
- 无处不在的代码提示;
- 灵活的拦截器;
- 可以创建多个实例,灵活根据项目进行调整;
- 每个实例,或者说每个接口都可以灵活配置请求头、超时时间等;
- 取消请求(可以根据 url 取消单个请求也可以取消全部请求)。
版本一:基础封装
// index.ts
import axios from "axios";
import type { AxiosInstance, AxiosRequestConfig } from "axios";
class Request {
// axios 实例
instance: AxiosInstance;
constructor(config: AxiosRequestConfig) {
this.instance = axios.create(config);
}
request(config: AxiosRequestConfig) {
return this.instance.request(config);
}
}
export default Request;版本二:拦截器封装
首先我们封装一下拦截器,这个拦截器分为三种:
- 类拦截器
- 实例拦截器
- 接口拦截器
接下来我们就分别实现这三个拦截器。
值得注意的是在 axios 最新版本中中请求拦截器的类型已经从 AxiosRequestConfig 变成了 InternalAxiosRequestConfig,下文中还是使用的 AxiosRequestConfig,不过在最后的源码中已经更新。
1. 类拦截器
类拦截器比较容易实现,只需要在类中对 axios.create() 创建的实例调用 interceptors 下的两个拦截器即可,实例代码如下:
// index.ts
constructor(config: AxiosRequestConfig) {
this.instance = axios.create(config)
this.instance.interceptors.request.use(
(res: AxiosRequestConfig) => {
console.log('全局请求拦截器')
return res
},
(err: any) => err,
)
this.instance.interceptors.response.use(
// 因为我们接口的数据都在res.data下,所以我们直接返回res.data
(res: AxiosResponse) => {
console.log('全局响应拦截器')
return res.data
},
(err: any) => err,
)
}我们在这里对响应拦截器做了一个简单的处理,就是将请求结果中的 .data 进行返回,因为我们对接口请求的数据主要是存在在 .data 中,跟 data 同级的属性我们基本是不需要的。
2. 实例拦截器
实例拦截器是为了保证封装的灵活性,因为每一个实例中的拦截后处理的操作可能是不一样的,所以在定义实例时,允许我们传入拦截器。
首先我们定义一下 interface,方便类型提示,代码如下:
// types.ts
import type { AxiosRequestConfig, AxiosResponse } from "axios";
export interface RequestInterceptors {
// 请求拦截
requestInterceptors?: (config: AxiosRequestConfig) => AxiosRequestConfig;
requestInterceptorsCatch?: (err: any) => any;
// 响应拦截
responseInterceptors?: (config: AxiosResponse) => AxiosResponse;
responseInterceptorsCatch?: (err: any) => any;
}
// 自定义传入的参数
export interface RequestConfig extends AxiosRequestConfig {
interceptors?: RequestInterceptors;
}定义好基础的拦截器后,我们需要改造我们传入的参数的类型,因为 axios 提供的 AxiosRequestConfig 是不允许我们传入拦截器的,所以说我们自定义了 RequestConfig,让其继承与 AxiosRequestConfig。
剩余部分的代码也比较简单,如下所示:
// index.ts
import axios, { AxiosResponse } from "axios";
import type { AxiosInstance, AxiosRequestConfig } from "axios";
import type { RequestConfig, RequestInterceptors } from "./types";
class Request {
// axios 实例
instance: AxiosInstance;
// 拦截器对象
interceptorsObj?: RequestInterceptors;
constructor(config: RequestConfig) {
this.instance = axios.create(config);
this.interceptorsObj = config.interceptors;
this.instance.interceptors.request.use(
(res: AxiosRequestConfig) => {
console.log("全局请求拦截器");
return res;
},
(err: any) => err
);
// 使用实例拦截器
this.instance.interceptors.request.use(
this.interceptorsObj?.requestInterceptors,
this.interceptorsObj?.requestInterceptorsCatch
);
this.instance.interceptors.response.use(
this.interceptorsObj?.responseInterceptors,
this.interceptorsObj?.responseInterceptorsCatch
);
// 全局响应拦截器保证最后执行
this.instance.interceptors.response.use(
// 因为我们接口的数据都在res.data下,所以我们直接返回res.data
(res: AxiosResponse) => {
console.log("全局响应拦截器");
return res.data;
},
(err: any) => err
);
}
}拦截器的执行顺序为实例请求 → 类请求 → 实例响应 → 类响应;这样我们就可以在实例拦截上做出一些不同的拦截。
3. 接口拦截
现在我们对单一接口进行拦截操作,首先我们将 AxiosRequestConfig 类型修改为 RequestConfig 允许传递拦截器;然后我们在类拦截器中将接口请求的数据进行了返回,也就是说在request()方法中得到的类型就不是 AxiosResponse 类型了。
我们查看 axios 的 index.d.ts 中,对 request() 方法的类型定义如下:
// type.ts
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;也就是说它允许我们传递类型,从而改变 request() 方法的返回值类型,我们的代码如下:
// index.ts
request<T>(config: RequestConfig): Promise<T> {
return new Promise((resolve, reject) => {
// 如果我们为单个请求设置拦截器,这里使用单个请求的拦截器
if (config.interceptors?.requestInterceptors) {
config = config.interceptors.requestInterceptors(config)
}
this.instance
.request<any, T>(config)
.then(res => {
// 如果我们为单个响应设置拦截器,这里使用单个响应的拦截器
if (config.interceptors?.responseInterceptors) {
res = config.interceptors.responseInterceptors<T>(res)
}
resolve(res)
})
.catch((err: any) => {
reject(err)
})
})
}这里还存在一个细节,就是我们在拦截器接受的类型一直是 AxiosResponse 类型,而在类拦截器中已经将返回的类型改变,所以说我们需要为拦截器传递一个泛型,从而使用这种变化,修改 types.ts 中的代码,示例如下:
// index.ts
export interface RequestInterceptors {
// 请求拦截
requestInterceptors?: (config: AxiosRequestConfig) => AxiosRequestConfig;
requestInterceptorsCatch?: (err: any) => any;
// 响应拦截
responseInterceptors?: <T = AxiosResponse>(config: T) => T;
responseInterceptorsCatch?: (err: any) => any;
}请求接口拦截是最前执行,而响应拦截是最后执行。
版本三:封装请求方法
现在我们就来封装一个请求方法,首先是类进行实例化示例代码如下:
// index.ts
import Request from "./request";
const request = new Request({
baseURL: import.meta.env.BASE_URL,
timeout: 1000 * 60 * 5,
interceptors: {
// 请求拦截器
requestInterceptors: config => {
console.log("实例请求拦截器");
return config;
},
// 响应拦截器
responseInterceptors: result => {
console.log("实例响应拦截器");
return result;
}
}
});然后我们封装一个请求方法, 来发送网络请求,代码如下:
// src/server/index.ts
import Request from "./request";
import type { RequestConfig } from "./request/types";
interface YWZRequestConfig<T> extends RequestConfig {
data?: T;
}
interface YWZResponse<T> {
code: number;
message: string;
data: T;
}
/**
* @description: 函数的描述
* @interface D 请求参数的interface
* @interface T 响应结构的intercept
* @param {YWZRequestConfig} config 不管是GET还是POST请求都使用data
* @returns {Promise}
*/
const ywzRequest = <D, T = any>(config: YWZRequestConfig<D>) => {
const { method = "GET" } = config;
if (method === "get" || method === "GET") {
config.params = config.data;
}
return request.request<YWZResponse<T>>(config);
};
export default ywzRequest;该请求方式默认为 GET,且一直用 data 作为参数。
参考文章
作者 GitHub:mqyqingfeng (冴羽)
TypeScript 文章 GitHub 仓库:TypeScript 从入门到进阶系统教程
林不渡
