📖TypeScript 教程 @阮一峰
TS - 概述
TypeScript 是由微软开发的一种基于 JavaScript 的编程语言,可看成是 JavaScript 的超集。相较于 JavaScript,TypeScript 增加了一个独立的类型系统。
类型(type)指的是一组具有相同特征的值,是人为添加的一种编程约束和用法提示。由于变量的类型和对象的属性是动态的,JavaScript 是动态类型语言,与之相反,TypeScript 是静态类型语言。
TS - 基本使用
1. 类型声明
-
变量类型声明
1
let foo: string;
补充:TypeScript 规定,变量只有赋值后才能使用,否则就会报错
-
函数类型声明(参数、返回值)
1
2
3function toString(num: number): string {
return String(num);
}
2. 类型推断
-
变量类型推断
1
let foo = 123; // foo 被推断为 number 类型
-
函数类型推断(返回值)
1
2
3function toString(num: number) { // toString 返回值被推断为 string 类型
return String(num);
}
3. 代码运行
3.1 tsc 编译器
-
解释:TypeScript 代码只有转换为 JavaScript 代码后,才能在浏览器和 Node.js 中运行,这个过程叫做编译(compile)。TypeScript 官方提供的编译器为
tsc
,可以将.ts
脚本转变为.js
脚本。- TypeScript 编译为 JavaScript 时,会删除全部类型声明和类型相关的代码,只留下可以运行的 JavaScript 代码。
- TypeScript 的类型检查是编译时的类型检查,而不是运行时的类型检查。
-
安装
npm install -g typescript
补充:安装后,可以使用
tsc -v
或tsc --version
查看tsc
版本,从而检查是否安装成功;使用tsc -h
或tsc --help
查看基本帮助信息,tsc --all
查看完整帮助信息 -
编译
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# 基本使用
tsc app.ts # 编译一个 ts 脚本,到当前目录
tsc file1.ts file2.ts file3.ts # 编译多个 ts 脚本,到当前目录
# 带编译参数使用
# --outFile xxx.js 指定编译后的 js 脚本名(默认与源文件一一对应且同名)
tsc file1.ts file2.ts --outFile app.js # 编译多个 ts 脚本为一个 js 脚本,到当前目录
# --outDir xxx 指定编译后的 js 脚本的保存位置(默认保存在当前目录)
tsc app.ts --outDir dist # 编译一个 ts 脚本,到指定目录
# --target xxx 指定编译后的 js 脚本对应的版本(默认编译为低版本的 JavaScript,兼容性考量)
tsc --target es2015 app.ts # 编译一个 ts 脚本,到当前目录,同时控制编译后的 JavaScript 版本
# --noEmitOnError 一旦报错就停止编译,同时不生成编译产物(默认情况不会停止编译,且会生成编译产物)
tsc --noEmitOnError app.ts # 编译一个 ts 脚本,到当前目录,如果报错就停止编译,且不生成编译产物
## --noEmit 只检查类型是否正确,不生成 js 脚本
tsc --noEmit app.ts # 编译一个 ts 脚本,但是不生成 js 脚本补充:TypeScript 允许使用配置文件
tsconfig.json
存储tsc
的编译参数。只要当前目录有该文件,那么tsc
就会在编译时自动读取,而不需要再显式地指定参数。依赖追踪:编译脚本时,tsc 会自动编译脚本所依赖的所有脚本。
注意:
tsc app.ts
时,编译器忽略任何tsconfig.json
,使用默认编译选项!!!
3.2 ts-node 模块
-
解释:
ts-node
是一个非官方的,可以直接运行 TypeScript 代码的模块。 -
安装
npm install -g ts-node
-
使用
1
2
3ts-node script.ts # 方式一:运行 ts 脚本 script.ts
npx ts-node script.ts # 方式二:不安装 ts-node 时,使用 npx 来在线运行 ts 脚本
ts-node # 方式三:打开一个 ts 脚本地 REPL 运行环境注意:使用
ts-node
运行ts
脚本时,需要全局安装typescript
包,并在项目根目录下添加配置文件tsconfig.json
。注意:使用
ts-node
运行ts
脚本时,且该ts
脚本是一个 ES6 模块,那么可能会报错!!这是因为 Node.js 支持 ES6 模块是一个实验性的功能,而ts-node
是根据 Node.js 版本进行适配的。因此一个可行的策略是降低 Node.js 版本到 v18.17.1。(最好在 tsconfig.json 中也补充了配置 {"ts-node": { esm: true } }
,以使得ts-node
以 ES6 模块化解析ts
脚本)
TS - 类型系统 1(特殊、基本、包装、Object、值、联合、交叉、type、typeof、作用域、兼容)
1. 特殊类型
1.1 any
-
解释:
any
类型表示没有任何限制。一旦将变量类型设为any
,TypeScript 就会关闭对该变量的类型检查,并且此变量可以被赋予任意类型的值,一般不推荐使用该类型。any
类型可以看成是所有其他类型的全集,因此 TypeScript 将该类型称为顶层类型(top type)。1
2
3
4
5
6
7
8
9let x: any;
/* 变量的类型检查被关闭(往往不是我们所希望的) */
x(1); // √
x.foo = "hello"; // √
/* 变量可以被赋予任意类型的值 */
x = 1; // √
x = true; // √ -
适用场景
- 出于某些特殊原因,需要关闭部分变量的类型检查。
- 为适配老的 JavaScript 项目,使代码能快速迁移到 TypeScript 。
-
存在的问题(不建议使用 any 类型的原因)
-
类型推断:我们知道,TypeScript 会推断那些没有进行类型声明的变量,如果无法推断,TypeScript 就会将相应变量的类型认定为
any
。为了避免这种问题,可以在使用tsc
编译器进行编译时,使用编译参数--noImplicitAny
,此时只要推断出any
类型就会报错。但需要注意的是,使用let
和var
声明变量时,如果不赋值,也不指定类型,TypeScript 就会推断其类型为any
,且不会报错,为了解决这个安全隐患,使用let
和var
声明变量时,要么赋值,要么显式声明类型。1
const add = (x, y) => x + y; // 函数参数和返回值都被推断为 any 类型,可能会导致错误
1
tsc --noImplicitAny app.ts # 编译 app.ts 脚本,当推断出 any 类型的变量时报错
-
类型污染:
any
类型的变量能够赋值给其他任何类型的变量,进而污染具有正确类型的变量,把错误留到了运行时。1
2
3
4let x: any = "hello";
let y: number;
y = x; // 类型污染,number 类型的变量 y 此时的值是一个字符串,可能会导致错误
-
1.2 unknown
-
解释:
unknown
类型可被视作严格版的any
类型 ,它与any
类型存在相似之处,同时也增添了一些限制。unknown
类型可以看成是所有其他类型(除了any
类型)的全集,因此 TypeScript 也将该类型称为顶层类型(top type)。-
相似之处:所有类型的值均可赋值给
unknown
类型的变量。1
2
3
4let x: unknown;
x = true; // √
x = 1; // √ -
限制之处
-
unknown
类型的变量不能直接赋值给其他类型的变量(除了any
和unknown
类型),从而避免了 any 类型中存在的类型污染问题。1
2
3let v: unknown = 123;
let v1: boolean = v; // × -
unknown
类型的变量的属性和方法不能直接被调用,从而避免了any
类型的类型检查关闭所带来的问题。1
2
3let v: unknown = { foo: 123 };
v.foo; // × -
unknown
类型的变量只能进行有限类型的运算,如==
、===
、!=
、!==
、||
、&&
、?
、!
、typeof
、instanceof
。1
2
3
4let v: unknown = 1;
a + 1; // ×
a === 1; // √ -
unknown
类型的变量只有经过类型缩小才可以使用,所谓类型缩小,就是缩小unknown
类型变量的类型范围,以确保不会出错。通常使用条件判断语句和typeof
运算符来缩小unknown
类型变量的类型范围。1
2
3
4
5
6
7let a: unknown = 1;
if (typeof a === 'number') {
let r = a + 10; // ✓
} else if (typeof a === 'string') {
console.log(a.length); // ✓
}
-
-
-
适用场景:
unknown
类型可以被看作是更安全的any
类型,因此凡是需要设定为any
类型的变量,通常都应优先考虑设为unknown
类型。
1.3 never
解释:never
类型表示空类型,即该类型不包含任何值。never
类型可以看成是空集,因此该类型是任何其他类型所共有的,TypeScript 将该类型称为底层类型(bottom type)。
1 | /* 不可以赋任何值给 never 类型的变量 */ |
2. 基本类型
类型 | 可取值 | 类型声明 |
---|---|---|
boolean |
true 和 false |
const x: boolean = true |
string |
所有字符串 | const x: string = 'hello' |
number |
所有整数和浮点数 | const x: number = 0xffff |
bigint |
所有大整数 | const x: bigint = 123n |
symbol |
所有的 Symbol 值 |
const x: symbol = Symbol(); |
object |
所有的对象、数组和函数 | const x: object = { foo: 123 } const y: object = [1, 2, 3] const z: object = (n: number) => n + 1 |
undefined |
undefined ,表示未定义 |
const x: undefined = undefined |
null |
null ,表示空 |
const x: null = null |
补充说明
bigint
类型与number
类型不兼容。bigint
类型由 ES2020 标准引入,如果使用该类型,tsc
编译器的目标 JavaScript 版本不能低于 ES2020(即target
参数不低于es2020
)。undefined
和null
即可以作为值,也可以作为类型。undefined
和null
能够赋值给其他任何类型的变量。可以使用--strictNullChecks
编译选项来规避这种情况,此时undefined
和null
无法赋值给其他类型的变量了(除了any
和unknown
类型的变量)- 当一个没有声明类型的变量被赋值
undefined
或null
时,不同的编译选项可能会导致不同的类型推断。- 关闭
--noImplicitAny
和--strictNullChecks
,变量被推断为any
。 - 打开
--strictNullChecks
,值为undefined
的变量被推断为undefined
类型,值为null
的变量被推断为null
类型。
- 关闭
3. 包装对象类型
-
原始类型(primitive type):表示最基本的、不可再分的值,包括:
boolean
、string
、number
、bigint
、symbol
。 -
包装对象(wrapper object):五种原始类型的值均有其对应的包装对象,也就是原始类型的值在必要时会自动转换而成的对象。
补充:可以通过
new Boolean()
、new String()
、new Number()
的方式获取boolean
、string
、number
类型的值所对应的包装对象;可以通过Object(Symbol())
、Object(BigInt())
的方式获取symbol
和bigint
类型的值对应的包装对象。补充:原始类型的值及其包装对象又可称之为字面量和包装对象,如
"hello"
是字面量,其包装对象为new String("hello")
。 -
包装对象类型:五种原始类型对应的包装对象的类型,用大写表示,包括
Boolean
、String
、Number
、BigInt
、Symbol
。包装对象类型包含包装对象和字面量两种情况,原始类型只包含字面量。1
2
3
4
5const s1: String = 'hello'; // √ 包装类型 - 字面量
const s2: String = new String('hello'); // √ 包装类型 - 包装对象
const s3: string = 'hello'; // √ 原始类型 - 字面量
const s4: string = new String('hello'); // × 原始类型 - 包装对象补充:建议只使用原始类型,不使用包装对象类型。
补充:
symbol
和Symbol
之间,以及bigint
和BigInt
之间没有差异。
4. Object/object
-
Object
类型:表示 JavaScript 中的广义对象,即所有能够转变为对象的值皆为Object
类型(除了undefined
和null
)。空对象{}
是Object
类型的简写形式。补充:
Object
类型的对象可以接受各种类型的属性,但是不能读取,否则会报错。 -
object
类型:表示 JavaScript 中的狭义对象,仅包含对象、数组和函数。
5. 值类型
-
值类型:将单个值作为一种类型,称其为 “值类型”。当使用
const
声明变量,并给该变量赋一个原始值时,TypeScript 就会推断该变量的类型为值类型。1
const x = 'https'; // x 的类型被自动推断为 "https"
补充:如果
const
声明的变量所赋的值为对象,那么 TypeScript 则不会推断该变量的类型为值类型。 -
与值类型有关的报错
1
2
3
4const x: 5 = 4 + 1; // ×,因为 x 的类型被认定是 5,而 4 + 1 的类型被推测为 number,5 是 number 的子类型,number 是 5 的父类型,父类型不能赋值给子类型,子类型能够赋值给父类型。在此由于将 number 这个父类型赋值给 5 这个子类型,所以发生报错。
/* 修正方式:使用 “类型断言”,即告诉 TypeScript 4 + 1 的类型是 5,而不是 number */
const x: 5 = (4 + 1) as 5; // √
6. 联合类型
-
解释:多个类型通过符号
|
能够组成一个新的类型,称作联合类型(union types)。任意一个值只要属于A
类型或者B
类型,那么也就属于联合类型A | B
,其中A
和B
是合法的 TypeScript 类型。 -
语法
-
单行书写
1
let x: A | B | C | ...;
-
多行书写
1
2
3
4
5let x:
| A
| B
| C
| ...;
-
7. 交叉类型
-
解释:多个类型通过符号
&
能够组成一个新的类型,称作交叉类型(intersection tyoe)。任意一个值同时属于 A 类型和 B 类型,那么也就属于交叉类型A & B
,其中A
和B
是合法的 TypeScript 类型。 -
语法
1
let x: A & B & C & ...;
-
适用场景:为对象类型添加新属性
1
2
3type A = { foo: number }; // A 类型表示具有 number 类型的 foo 属性的对象
type B = A & { bar: number }; // B 类型对 A 类型进行扩展,要求对象还要有 number 类型的 bar 属性
8. 类型别名 - type
-
解释:TypeScript 中允许通过
type
关键字来定义一个类型的别名。1
2type Gender = "Male" | "Female"; // Gender 类型,取值范围为 "Male" 和 "Female"
type Weather = "Sunny" | "Clear" | "Rainy"; -
注意事项
-
类型别名的作用域是块级作用域。
1
2
3
4type Color = 'red';
if (Math.random() < 0.5) {
type Color = 'blue'; // ×
} -
在同一作用域中,类型别名不允许重名。
1
2type Color = 'red';
type Color = 'blue'; // × -
类型别名允许嵌套。
1
2
3
4
5type Country = "China";
type Province = "Shaan'xi";
type Address = `Xi'an ${Province} ${Country}`;
let x: Address = "Xi'an Shaan'xi China";
-
9. 类型运算 - typeof
-
解释:
typeof
运算符在 JavaScript 和 TypeScript 中具有不同的功能。在 JavaScript 中,typeof
接受一个操作数,返回一个字符串,代表操作数的类型。在 TypeScript 中,typeof
也接受一个操作数,但是返回的是操作数的 TypeScript 类型。前者称之为值运算,后者称之为类型运算。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/* 值运算 - JavaScript 中的 typeof */
typeof undefined; // "undefined"
typeof true; // "boolean"
typeof 13; // "number"
typeof "foo"; // "string"
typeof {}; // "object"
typeof parseInt; // "function"
typeof Symbol(); // "symbol"
typeof 13n; // "bigint"
/* 类型运算 - TypeScript 中的 typeof */
const a = { x: 0};
type T0 = typeof a; // { x: number }
type T1 = typeof a.x; // number -
注意事项
-
TypeScript 代码中,可能同时存在两种
typeof
运算符,即一种用于值运算,一种用于类型运算。1
2
3
4
5let a = 1;
let b: typeof a; // 类型运算
if(typeof a === 'number')
b = a; // 值运算 -
TypeScript 代码编译后,用于值运算的
typeof
运算符会被保留,用于类型运算的typeof
运算符会被全部删除。1
2
3
4
5
6/* 上一个 ts 示例代码的编译结果 */
let a = 1;
let b; // 类型运算 - typeof 被删除
if(typeof a === 'number')
b = a; // 值运算 - typeof 被保留 -
TypeScript 代码在编译时不会进行 JavaScript 的值运算,所以用于类型运算的
typeof
运算符的操作数不能是需要进行运算的表达式。此外,用于类型运算的typeof
操作符的操作数不能是类型。1
2
3
4type T = typeof Date(); // ×
type Age = number;
type MyAge = typeof Age; // ×
-
10. 类型作用域 - 块级
解释:TypeScript 支持块级类型声明,即类型可以声明在代码块中,并且只在当前代码块中生效。
11. 类型兼容
解释:TypeScript 中规定子类型兼容父类型,即凡是可以使用父类型的地方,都可以使用子类型,反之则不行。如 number | string
就是 number
的父类型。
TS - 类型系统 2(数组、元组、symbol)
1. 数组
1.1 基本使用
-
数组(array):TypeScript 中的数组规定所有成员的类型必须相同,成员数量不定。
-
语法
-
elementType[]
或(elementType)[]
1
2let arr1: number[] = [1, 2, 3];
let arr2: (number | string)[] = [1, "hello", 3]; // 考虑到运算符优先级,复杂的成员类型需要写在括号中 -
Array<elementType>
1
2let arr1: Array<number> = [1, 2, 3];
let arr2: Array<number | string> = [1, "hello", 3];
-
-
注意事项
-
数组成员的数量可以动态变化,因此对数组进行越界访问并不会报错。
-
TypeScript 使用
elementType[][]
的形式表示二维数组。 -
TypeScript 允许通过索引读取数组成员的类型,也就是对数组类型进行索引访问。
1
2
3
4
5
6
7/* 情况一:访问指定索引的成员类型 */
type Names = string[];
type Name = Names[0]; // string
/* 情况二:访问指定类型的索引的成员类型 */
type Names2 = string[];
type Name2 = Names2[number]; // string -
当变量的初始值为空数组时,TypeScript 会推断该变量的类型为
any[]
,之后每当给该数组赋值,TypeScript 都会自动更新对该变量的类型推断。1
2
3const arr = []; // 推断类型为 any[]
arr.push(1); // 推断类型为 number[];
arr.push("HELLO"); // 推断类型为 (string | number)[]
-
1.2 只读数组
-
只读数组:相较于普通数组,TypeScript 规定只读数组没有
pop()
、push()
等会改变原数组的方法,因此也称只读数组为普通数组的父类型(成员类型需相同),普通数组是只读数组的子类型。 -
语法
-
readonly elementType[]
或readonly (elementType)[]
-
ReadonlyArray<elementType>
-
Readonly<elementType[]>
-
[] as const
(const
断言,只读的值类型)1
2
3
4let arr1: readonly number[] = [1, 2, 3];
let arr2: ReadonlyArray<number> = [1, 2, 3];
let arr3: Readonly<number[]> = [1, 2, 3];
let arr4 = [1, 2, 3] as const; // 不能写成 let arr4: number[] = [1, 2, 3] as const !
-
-
注意事项:由于只读数组是普通数组的父类型(成员类型相同),因此如果在需要普通数组的地方使用只读数组,则应使用
as
关键字进行类型断言。1
2
3const func = (arr: number[]) => {}
const arr: readonly number[] = [1, 2, 3];
func(arr as number[]); // 父类型 readonly number[],子类型 number[]
2. 元组
2.1 基本使用
-
元组(tuple):TypeScript 中的元组就是成员类型可以自由设置的数组,因此元组的每个成员的类型都必须明确声明。
-
语法
-
[elementType1, elementType2, elementType3]
-
[elementType1, elementType2, elementType3?]
(可选成员,必须位于必选成员之后) -
[elementType1, ...elementType2, elementType3[]]
(不限数量的成员,可以位于任意位置) -
[elementName1: elementType1, elementName2: elementType2, elementName3: elementType3]
(指定成员名,仅起到说明作用)1
2
3
4const t1: [number, string, boolean] = [1, "hello", true];
const t2: [number, string, boolean?] = [1, "hello"];
const t3: [number, ...string[], boolean] = [1, "hello", "world", true];
const t4: [num: number, msg: string, flag: boolean] = [1, "hello", true];
-
-
注意事项
-
TypeScript 中数组的成员类型写在方括号外(如
number[]
),元组的成员类型写在方括号里(如[number]
)。 -
使用元组时必须给出类型声明,不然 TypeScript 会将其自动推断为一个数组。
-
TypeScript 会根据元组的成员类型推断成员数量(或元组长度)。
1
2
3
4
5
6
7
8
9
10
11/* 1. 元组没有可选成员和扩展运算符,TypeScript 会推断出元组的成员数量 */
let t1: [number, number] = [0, 1];
t1.length === 3; // ×,只能是 2
/* 2. 元组包含可选成员,TypeScript 会推断出元组的可能的成员数量 */
let t2: [number, number?] = [0];
t2.length === 3; // ×,可以是 1 或 2
/* 3. 元组中包含使用了扩展运算符的数组或元组,TypeScript 无法推断出元组的成员数量 */
let t3: [...number[]] = [1, 2, 3];
t3.length === 9; // √ -
扩展运算符可以将数组或元组转换为一个逗号分隔的序列。TypeScript 无法确定数组转换的序列的成员数量,但是可以确定元组转换的序列的成员数量,以及
const
断言的值类型转换的序列的成员数量。1
2
3
4
5
6
7
8
9
10
11
12
13
14const func = (a: number, b: number) => { }
/* 1. 数组转换的序列的成员数量无法确定 */
let a: number[] = [1, 2, 3];
func(...a); // ×,因为 TypeScript 无法确定 a 转换的序列的成员数量为 2
/* 2. 元组转换的序列的成员数量可以确定 */
let b: [number, number] = [1, 2];
func(...b); // √,因为 TypeScript 可以确定 b 转换的序列的成员数量为 2
/* 3. const 断言转换的序列的成员数量可以确定 */
let c = [1, 2] as const; // c 的 类型为 readonly [1, 2]
func(...c);; // √,因为 TypeScript 可以确定 c 转换的序列的成员数量为 2[] as const
断言的类型是只读的值类型,即可以当作只读数组,也可以当作只读元组。 -
因为在使用元组时需要声明每个成员的类型,大多数情况下元组的成员数量有限,所以此时不允许对元组进行越界访问。
1
2
3const t: [number, boolean, string] = [1, true, "hello"];
t[1]; // √
t[3]; // × -
TypeScript 允许通过索引读取元组成员的类型,也就是对元组类型进行索引访问。
1
2
3
4
5
6
7/* 情况一:访问指定索引的成员类型 */
type Eles = [string, number];
type Ele = Eles[1]; // number
/* 情况二:访问指定类型的索引的成员类型 */
type Eles2 = [string, number];
type Ele2 = Eles2[number]; // string | number
-
2.2 只读元组
-
只读元组:与只读数组类似,只读元组是普通元组的父类型(成员类型需对应相同),普通元组是只读元组的子类型。
-
语法
-
readonly [elementType1, elementType2, elementType3]
-
Readonly<[elementType1, elementType2, elementType3]>
-
[] as const
(const
断言,只读的值类型)1
2
3let t1: readonly [number, boolean] = [1, true];
let t2: Readonly<[number, boolean]> = [1, true];
let t3 = [1, true] as const;
-
-
注意事项:由于只读元组是普通元组的父类型(成员类型需对应相同),因此如果在需要普通元组的地方使用只读元组,则应使用
as
关键字进行类型断言。1
2
3const func = (t: [number, number]) => { };
const t: readonly [number, number] = [1, 2];
func(t as [number, number]);
3. symbol
3.1 基本介绍
JavaScript 中的 Symbol
是 ES2015 引入的一种新的原始类型的值,其特点是:每一个 Symbol
值都是唯一的,与其他任何值都不相等。
JavaScript 中的 Symbol
值可以通过 Symbol()
函数生成。也可以通过 Symbol.for(key: string)
函数生成,相同的 key
对应相同的 Symbol
值。
TypeScript 中使用 symbol
类型来表示 Symbol
值。
3.2 unique symbol
-
解释:
unique symbol
是symbol
的一个子类型,用于表示单个的,某个具体的Symbol
值。类型为unique symbol
的变量只能使用const
命令声明。1
2const x: unique symbol = Symbol(); // √
let y: unique symbol = Symbol(); // × -
注意事项
-
使用
const
关键字声明变量,并赋Symbol
值时,变量类型被自动推断为unique symbol
。可以简化unique symbol
类型的变量声明。1
const z = Symbol(); // 等价于 const z: unique symbol = Symbol();
-
每个声明为
unique symbol
类型的变量的值不同,类型也不同!1
2
3
4const a: unique symbol = Symbol();
const b: unique symbol = Symbol();
a === b; // × -
因为
Symbol.for(key)
在key
相同时返回相同的Symbol
值,因此可能会导致多个unique symbol
类型的变量的值相同。1
2
3
4const p: unique symbol = Symbol.for('hello');
const q: unique symbol = Symbol.for('hello');
p === q; // 虽然 p 和 q 的值相同,但是其类型不同,因此不能作 === 运算 -
unique symbol
类型是symbol
类型的子类型,因此在使用symbol
的时候可以使用unique symbol
,反之则不行。 -
unique symbol
类型的值可以作为对象的属性名,而symbol
类型的值不行。1
2
3
4
5
6
7const m: unique symbol = Symbol();
let n: symbol = Symbol();
type Foo = {
[m]: string; // unique symbol 可以作为属性名
[n]: string; // symbol 不可以作为属性名
} -
可以通过
static readonly fieldName: unique symbol = Symbol()
的方式声明一个类型为unique symbol
的静态只读属性。1
2
3class Person {
static readonly foo: unique symbol = Symbol();
} -
将
Symbol()
函数的值赋给一个let
声明的变量,其类型将会被推断为symbol
;赋给一个const
声明的变量,其类型将会被推断为unique symbol
。1
2let j = Symbol(); // symbol
const k = Symbol(); // unique symbol -
将
symbol
或unique symbol
类型的变量赋值给const
或let
声明的变量,其类型都会被推断为symbol
。1
2
3
4
5let e = Symbol();
const f = e; // symbol
const g = Symbol();
let h = g; // symbol
-
TS - 类型系统 3(函数、对象)
1. 函数
1.1 基本使用
-
解释:函数类型声明,即在声明函数时,给出参数的类型和返回值的类型。
-
语法
-
function(para1: paraType1, para2: paraType2, ...): returnType{}
(函数声明)1
const speakHi = function (msg: string): void { console.log(msg) }
-
(para1: paraType1, para2: paraType2,...) => returnType
(函数类型,一般写法)1
const speakHello: (msg: string) => void = function (msg) { console.log(msg) }
-
{ (para1: paraType1, para2: paraType2,...): returnType }
(函数类型,对象写法)1
const speakWelcome: { (msg: string): void } = function (msg) { console.log(msg) }
补充:当函数本身存在属性时,多使用这种函数类型声明方式
-
type FuncTypeName = (para1: paraType1, para2: paraType2,...) => returnType
或type FuncTypeName = { (para1: paraType1, para2: paraType2,...): returnType }
(函数类型,type
写法)1
2type func = { (msg: string): void }
const speakWelcome: func = function (msg) { console.log(msg) } -
interface FuncTypeName { (para1: paraType1, para2: paraType2,...): returnType }
(函数类型,interface
写法)1
2interface IFunc { (msg: string): void };
const speakYes: IFunc = function (msg) { console.log(msg) }
-
-
使用说明
-
如果不指定参数类型,TypeScript 会推断参数类型为
any
;返回值类型通常可以被 TypeScript 自动推断而得出。 -
函数类型中的参数名称可以与实际函数的参数名称不一致。
1
const printf: (a: any, b: any) => void = (para1, para2) => { console.log(para1, para2) }
-
函数类型中的参数数量可以多于实际函数的参数数量,但不能小于。
1
2
3type func = (a: any, b: any) => void
const printf1: func = (para1) => { console.log(para1) } // √,实际函数参数数量可以少于
const printf2: func = (para1, para2, para3) => { console.log(para1, para2, para3) } // ×,实际函数参数数量不能多余! -
可以使用
typeof
运算符计算一个函数变量的函数类型,并将其用作另一个变量的类型声明。1
2const add: (a: number, b: number) => number = (a, b) => a + b;
const del: typeof add = (a, b) => a - b; -
函数内部允许声明其他类型,并只在该函数内部有效,称之为局部类型。
-
如果一个函数的返回值是一个函数,那么该函数称之为高阶函数(higher-order function)。
-
1.2 Function
Function
也是一种函数类型,任何函数都归属于该类型。Function
表示对函数不做任何约束,接受任意数量的参数,参数和返回值的类型皆为 any
。
1.3 特殊参数
可选参数
解释:参数名后加问号(?
)意味着该参数为可选参数,即调用函数时该参数能够省略。
1 | const sum: (a: number, b?: number) => number = (x, y) => { |
- 函数的可选参数只能处于参数列表的尾部,在必选参数之后。
- 可选参数的类型实际上为原始类型
|undefined
。
参数解构
解释:元组参数或对象参数在函数的参数列表中被拆解为若干参数,称其为参数解构。
1 | /* 解构元组 */ |
rest 参数
解释:参数名前加省略号(...
)意味着该参数为 rest
参数,表示函数中剩余的所有参数,该参数可分为数组(剩余参数类型相同)和元组(剩余参数类型不同)两种类型。其中,元组类型的 rest
参数需要声明每一个成员的类型,同时元组中的成员也可以标记为可选的。
1 | /* 数组类型的 rest 参数 */ |
只读参数
解释:参数类型前边加上 readonly
关键字意味着该参数为只读参数。目前,readonly
关键字只允许在数组和元组类型的参数上应用。
1 | function sum(arr: readonly number[]): number { |
1.4 特殊返回值
void
解释:该类型表示函数没有返回值。
-
void
类型允许函数返回undefined
或null
。但如果打开了strictNullChecks
编译选项,此时函数只能返回undefined
,否则会报错。1
2
3
4
5
6
7
8
9function func1(): void {
return undefined; // √
}
function func2(): void {
return null; // 如果打开 strictNullChecks 编译选项,则会报错
}
function func3(): void {
return 123; // ×
} -
如果变量、对象方法、函数参数的类型是一个返回值为
void
类型的函数,此时可以给其赋值一个有返回值的函数,并且不会报错。这是因为 TypeScript 认为此时的void
类型表示该函数的返回值没有利用价值,或者不应该使用该函数的返回值。因此,一旦使用了该函数的返回值,就会报错。联想:这样设计是具有现实意义的,比如数组方法
Array.prototype.forEach(fn)
的参数fn
就是一个返回值为void
的函数,其可以被赋值为一个有返回值的函数,其返回值不会被使用。1
2
3
4
5
6
7
8
9
10
11
12/* 返回值为 void 类型的函数变量被赋值为一个有返回值的函数 */
const func1: () => void = function () { return 123 };
/* 返回值为 void 类型的对象方法被赋值为一个有返回值的函数 */
class Person {
func2: () => void = function () { return 123 }
}
/* 返回值为 void 类型的函数参数被赋值为一个有返回值的函数 */
function log(func3: () => void) { func3() };
/* void 类型的函数,表示其返回值不应该被使用,否则就会报错 */
func1() * 2; // ×
(new Person()).func2() * 2; // × -
如果函数字面量的类型是一个返回值为
void
类型的函数,此时是不能有返回值的(undefined
和null
除外)。1
function func(): void { return 123 }; // ×
-
如果函数在运行中必定会报错,则可以将返回值类型写为
void
。 -
如果将一个变量类型设置为
void
,此时只能将其赋值为undefined
或null
(没有打开strictNullChecks
编译选项)。
never
解释:该类型表示肯定不会出现的值。如果函数抛出异常或陷入了死循环,那么该函数就无法正常返回一个值,此时函数的返回值类型就是 never
。
1 | /* 函数抛出异常 */ |
-
函数返回值类型为
never
,表示函数无法正常执行结束,不可能有返回值;函数返回值类型为void
,表示函数可以正常执行结束,但是不返回值(或者返回undefined
)。 -
程序中调用一个返回值类型为
never
的函数,会导致程序在该函数调用的位置终止,即不会继续执行后续的代码。 -
如果函数在某些情况下能够正常执行并返回值,在另一些情况下无法正常执行,此时其返回值类型可以是正常执行情况下的返回值。
1
2
3
4
5
6
7
8/* 以下函数的返回值类型实际上为 never|number,但是由于 never 是底层类型,never|number 等价于 number */
function func(): number {
if (Math.random() > 0.5) {
console.log("Success");
return 200;
}
throw new Error("Error");
}
1.5 函数重载
-
解释:有些函数(包含对象的方法)可以根据参数类型或个数不同而执行不同逻辑,称之为函数重载(function overload)。函数重载该有利于精确描述函数参数与返回值之间的对应关系。
-
语法:为了实现函数重载,TypeScript 要求提供多个函数重载声明和一个函数具体实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function func(a: paraType1, b: par2Type2): returnType1; // 重载声明 1
function func(a: paraType3, b: paraType4): returnType2; // 重载声明 2
/*
重载声明的对象写法
type func = {
(a: paraType1, b: par2Type2): returnType1;
(a: paraType3, b: paraType4): returnType2;
}
*/
function func(a: paraType1 | paraType2, b: paraType2 | paraType4): returnType1 | returnType2 { // 具体实现
if (typeof paraType1 === "paraType1" && typeof paraType2 === "paraType2") {
/* 重载声明 1 的具体实现 */
}
if (typeof paraType3 === "paraType3" && typeof paraType4 === "paraType4 ") {
/* 重载声明 2 的具体实现 */
}
throw new Error("非法的函数参数");
} -
使用说明
- 函数的重载声明和函数的具体实现之间不能包含其他代码,否则会报错。
- 因为 TypeScript 按顺序从上到下检查函数的重载声明,因此要将类型最宽的重载声明放在最下边,以防覆盖其他类型声明。
- 不使用重载声明也能够实现函数重载,但是此时会丧失参数与返回值之间存在的对应关系,所以如果参数的选择不影响返回值类型时,可以省略重载声明。
1.6 构造函数
构造函数的类型声明
- 语法1
type ConstructorTypeName = new (para1: paraType1, para2: paraType2, ...) => ObjectType
- 语法2
type ConstructorTypeName = { new (para1: paraType1, para2: paraType2, ...): ObjectType}
补充:如果一个函数即可以当作构造函数使用,又可以当作普通函数使用,则可以使用以下语法进行类型声明
1
2
3
4 type FuncTypeName = {
new (para1: paraType1, para2: paraType2, ...): ObjectType; // 构造函数类型
(para1: paraType1, para2: paraType2, ...): returnType; // 普通函数类型
}
2. 对象
2.1 基本使用
-
解释:对象类型声明,即在声明对象时,指定对象属性的类型和方法的类型。
-
语法
-
一般写法
1
2
3
4
5
6/* 语法 */
{
propertyName: propertyType; // 属性
methodName1: (para1: paraType1, para2: paraType2) => returnType1; // 方法 - 方式 I
methodName2(para1: paraType1, para2: paraType2): returnType2; // 方法 - 方式 II
}1
2
3
4
5
6
7
8
9
10
11
12/* 示例 */
const person: {
name: string;
age: number;
speak(): void;
introduce: () => void;
} = {
name: "Jack",
age: 19,
speak() { console.log("Hi! Nice to meet you~") },
introduce: () => { console.log("My name is Jack and I'm 19 years old!") },
} -
type
写法(即给对象类型起一个别名)1
2
3
4
5
6/* 语法 */
type ObjectTypeName = {
propertyName: propertyType; // 属性
methodName1: (para1: paraType1, para2: paraType2) => returnType1; // 方法 - 方式 I
methodName2(para1: paraType1, para2: paraType2): returnType2; // 方法 - 方式 II
}1
2
3
4
5
6
7
8
9
10
11/* 示例 */
type Animal = {
name: string;
weight: number;
bark(): void;
}
const dog: Animal = {
name: "Lucky",
weight: 9,
bark() { console.log("WangWang!") }
} -
interface
写法(即将对象类型提炼为一个接口)1
2
3
4
5
6/* 语法 */
interface ObjectTypeName {
propertyName: propertyType; // 属性
methodName1: (para1: paraType1, para2: paraType2) => returnType1; // 方法 - 方式 I
methodName2(para1: paraType1, para2: paraType2): returnType2; // 方法 - 方式 II
}1
2
3
4
5
6
7
8
9
10
11
12
13/* 示例 */
interface IStudent {
id: number;
className: number;
grade: number;
welcome: () => void;
}
const stu: IStudent = {
id: 121313,
className: 12,
grade: 3,
welcome() { console.log("Good Morning~") }
}
-
-
使用说明
-
TypeScript 使用大括号
"{}"
表示对象类型,同时在大括号内部声明每个属性和方法的类型。每个属性/方法的类型可以以分号";"
或逗号","
结尾,最后一个属性/方法后边可以省略分号或逗号。 -
与数组类似,TypeScript 允许通过属性/方法名读取对应属性/方法的类型,也就是对对象类型进行索引访问。
1
2
3
4
5type User {
name: string,
age: number
};
type Name = User['name']; // string -
在 TypeScript 里,若想在对对象进行解构赋值时明确解构变量的类型,得给所有要解构的变量添加上对象类型声明,不能直接给解构变量加类型,因为直接加类型的语法是用来设置解构变量别名的。
1
2
3
4
5
6
7
8
9
10const {
x: user_name, // 给解构变量 x 设置别名
y: user_age // 给解构变量 y 设置别名
}: {
x: string, // 给解构变量 x 声明类型
y: string // 给解构变量 y 声明类型
} = {
x: "Zhang'san",
y: "abdscdwhv12"
};
-
2.2 特殊属性
可选属性
-
解释:对象类型中,属性名后加一个问号
"?"
表示可选属性。1
const obj: { name: string, age?: number } = { name: "Jack" };
-
使用说明
-
可选属性的类型等同于属性本身类型 |
undefined
,即可选属性允许被赋值为undefined
。同时,一个未被赋值的可选属性的值为undefined
。因此,在使用对象的可选属性时,必须检查其是否为undefined
。 -
在使用可选属性前,可以使用三元运算符
"?:"
或空值合并运算符"??"
来判断其值是否为undefined
,并设置默认值。1
2
3
4
5
6
7
8
9
10
11
12const user: {
id: number,
firstName?: string,
lastName?: string
} = {
id: 12341,
firstName: "Jack",
lastName: "Brown"
}
const firstName = user.firstName === undefined ? "Tom" : user.firstName;
const lastName = user.lastName ?? "Wood";空值合并运算符
??
:左侧操作数为null
或undefined
时,返回其右侧的操作数,否则返回左侧的操作数。 -
如果同时使用编译选项
ExcatOptionsPropertyTypes
和strictNullChecks
,TypeScript 将不允许可选属性设置为undefined
。1
2
3
4
5
6
7const student: {
name: string,
age?: number
} = {
name: "Jack",
age: undefined // 报错,在打开 ExcatOptionsPropertyTypes 和 strictNullChecks 编译选项时,可选属性不能赋值为 undefined
}
-
只读属性
-
解释:对象类型中,属性名前面加上
readonly
关键字,表示只读属性,其只能在对象初始化期间赋值,之后不能被再修改。1
2const obj: { name: string, readonly age: number } = { name: "Jack", age: 12 };
obj.age = 13; // × -
使用说明
-
如果只读的属性是一个对象,TypeScript 不会禁止对该对象的属性进行修改,但会禁止对该对象进行替换。
1
2
3const stu: Student = { score: { chinese: 80, math: 80, english: 80, average: 80 } };
stu.score.average = 90; // ✓,允许修改只读的对象属性的属性
stu.score = { chinese: 80, math: 80, english: 80, average: 80 }; // ×,不允许替换只读的对象属性 -
如果两个变量(引用)指向同一个对象,且一个变量规定对象属性可写,另一个变量规定对象属性只读,此时,前者对对象属性的修改会影响到后者对对象属性的访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15interface WritablePerson {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let w: WritablePerson = { name: "Jack", age: 12 };
let r: ReadonlyPerson = w;
w.age++;
console.log(r.age); // 13 -
为了让对象的所有属性都是只读的(深度只读),在将对象赋值给变量时可以使用
as const
。但要注意,如果对变量添加了类型声明,TypeScript 会优先采用声明的类型,导致as const
失效。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41/* 使用只读断言的对象是深度只读的 */
const obj1 = {
name: "Jack", address: {
province: "He'nan",
city: "Xin'xiang",
detail: {
district: "Xin'cheng",
street: 2001,
}
}
} as const;
obj1.name = "Tom"; // ×
obj1.address.city = "Xin'yang"; // ×
obj1.address.detail.street = 9011; // ×
/* 类型声明会和只读断言发生冲突,以类型声明为准,只读断言失效 */
const obj2: {
name: string,
address: {
province: string,
city: string,
detail: {
district: string,
street: number
}
}
} = {
name: "Jack", address: {
province: "He'nan",
city: "Xin'xiang",
detail: {
district: "Xin'cheng",
street: 2001,
}
}
} as const;
obj2.name = "Tom"; // √
obj2.address.city = "Xin'yang"; // √
obj2.address.detail.street = 9011; // √
-
索引签名
-
解释:允许对象拥有任意数量的属性,同时使这些属性满足特定的键值约束,即索引签名。
-
语法:
{ [indexName: indexType]: valueType }
1
2
3
4
5
6
7
8
9/* 使用索引签名时还可以起一个类型别名(type),或将其提炼为一个接口(interface) */
type T1 = { [property: string]: string };
type T2 = { [property: number]: string };
type T3 = { [property: symbol]: string };
const t1: T1 = { name: "ZhangSan", gender: "Male" };
const t2: T2 = { 0: "Hello", 1: "World" };
const [first, second] = [Symbol(), Symbol()];
const t3: T3 = { [first]: "Happy", [second]: "New Year" };补充:
indexName
表示属性名,可以随便起;indexType
是属性名的类型,可以是string
、number
和symbol
;valueType
是属性值的类型。 -
使用说明
-
使用索引签名时,可以同时存在多种类型(属性名的类型不同)的索引。如果同时存在字符串索引和数值索引,必须以字符串索引的属性值类型为准,否则会引发冲突,这是因为 JavaScript 中会自动将数值属性名转换为字符串属性名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14interface T1 {
[property: string]: string;
[property: number]: string;
} // √
interface T2 {
[property: string]: string;
[property: number]: boolean; // 需要为 string
} // ×
interface T3 {
[property: string]: string;
[property: symbol]: number
} // √补充:这里的字符串索引和数值索引,指的是索引签名中属性名的类型,即
indexType
。 -
同时使用索引签名和特定属性的类型声明时,如果该属性与存在的索引的属性名的类型相同,则属性值的类型一定也要相同,否则会引发冲突。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25interface T1 {
name: string; // 需为 number;name 属性的属性名类型为 string,其属性值类型与字符串索引的属性值类型产生冲突,从而报错。
[detail: string]: number;
}; // ×,
interface T2 {
name: string;
[detail: string]: string;
}; // √
interface T3 {
0: string; // 需为 number;属性名类型为 number 的属性也会和字符串索引产生冲突!
[detail: string]: number;
}; // ×
const x = Symbol();
interface T4 {
[x]: string;
[detail: string]: number;
};
interface T5 {
name: string; // 属性名类型为 string 的属性不会和数值串索引产生冲突!(但是运行时可能会暴露问题,因此不推荐!)
[detail: number]: number;
}; // √ -
可以使用索引签名定义一个表示没有任何属性的对象的类型,代码如下。
1
2
3interface WithoutProperties {
[key: string | number | symbol]: never;
};
-
2.3 结构类型
-
结构类型原则(structural typing):在 TypeScript 中,若对象 B 具备对象 A 的结构特征,即认为对象 B 的类型兼容对象 A 的类型,这就是结构类型原则。若对象 B 的类型兼容对象 A 的类型,则使用对象A 的地方皆可使用对象 B,即对象 B 可赋值给对象 A 所对应的变量。
1
2
3
4
5
6/* Student 类型的对象兼容 Person 类型的对象 */
interface Person { name: string; age: number; };
interface Student { name: string; age: number; grade: number; };
const s = { name: "Jack", age: 19, grade: 9 };
const p = s;补充-1:TypeScript 根据结构类型原则检查某个值是否符合指定类型。
补充-2:如果类型 B 兼容类型 A,那么类型 B 就是类型 A 的子类型,类型 A 是类型 B 的父类型,即子类型兼容父类型。子类型具有父类型的所有结构特征,同时还具有自己的特征。凡是可以使用父类型的地方,都可以使用子类型。
-
严格字面量检查(strict object literal):将对象字面量赋给声明了对象类型的变量时,二者类型必须完全一致,否则会触发严格字面量检查报错。
1
2
3
4
5
6
7
8const p: {
name: string,
age: number,
} = {
name: "Jack",
age: 19,
height: 190, // ×
} -
规避严格字面量检查的方式
-
方式一:使用中间变量(要确保类型兼容!)
1
2
3
4
5
6
7
8
9
10const temp = {
name: "Jack",
age: 19,
height: 190,
}
const p: {
name: string,
age: number,
} = temp; // √,temp 兼容 p,也就是说 temp 是 p 的子类型,p 是 temp 的父类型。 -
方式二:使用类型断言(要确保类型兼容!)
1
2
3
4
5
6
7
8
9
10type Person = {
name: string,
age: number,
}
const p: Person = {
name: "Jack",
age: 19,
height: 190,
} as Person; // √ -
方式三:使用索引签名(要确保类型兼容!)
1
2
3
4
5
6
7
8
9
10
11type Person = {
name: string,
age: number,
[property: string]: any
}
const p: Person = {
name: "Jack",
age: 19,
height: 190,
}; // √ -
方式四:打开
suppressExcessPropertyErrors
编译选项,以关闭多余属性检查(要确保类型兼容,且只适用于函数传参!)1
2
3
4
5
6
7
8
9
10
11
12
13
14type Person = {
name: string,
age: number,
}
function logInfo(p: Person) {
console.log(`info: ${p.name}-${p.age}`);
}
logInfo({
name: "Jack",
age: 19,
height: 190,
}); // √补充:Option ‘suppressExcessPropertyErrors’ is deprecated and will stop functioning in TypeScript 5.5(编译选项
suppressExcessPropertyErrors
在 TypeScript 5.5 中被废除)。
-
2.4 最小可选属性
-
解释:如果某个对象类型的所有属性都是可选的,那么该类型的对象必须至少存在一个可选属性,即最小可选属性原则,又称弱类型检测(weak type detection)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14type Coordinate = {
x?: number;
y?: number;
z?: number;
};
const temp1 = { d: 100 };
const cor1: Coordinate = temp1; // ×
const temp2 = { x: 100, d: 100 };
const cor2: Coordinate = temp2; // √
const temp3 = {};
const cor3: Coordinate = temp3; // √补充 - 1:最小可选属性原则是对结构类型原则的补充,否则上述示例中的
cor1
变量不应该报错。补充 - 2:当某一类型所对应的对象,既 ① 未涵盖该类型下的所有可选属性,又 ② 存在超出其类型声明范围的其他属性,同时还需 ③ 满足结构类型原则,也就是类型兼容的要求时,此原则才会生效。
-
规避方式
-
方式一:使用索引签名(要确保类型兼容!)
1
2
3
4
5
6
7
8
9type Coordinate = {
x?: number;
y?: number;
z?: number;
[property: string]: any
};
const temp = { d: 100 };
const cor: Coordinate = temp; -
方式二:使用类型断言(要确保类型兼容!)
1
2
3
4
5
6
7
8type Coordinate = {
x?: number;
y?: number;
z?: number;
};
const temp = { d: 100 };
const cor: Coordinate = temp as Coordinate;
-
TS - 类型系统 4(interface、类)
1. interface
1.1 基本使用
-
解释:
interface
是对象的模板,用于指定对象的类型结构,译为接口。接口的使用又称为接口的实现,即将接口当作对象类型来使用。TypeScript 中通过interface
关键字来定义接口。1
2
3
4
5
6interface IStudent {
name: string;
age: number;
}
const jack: IStudent = { name: "Jack", age: 19 }; -
语法:
interface 接口名 = { 接口成员 }
补充:TypeScript 中使用
interface
关键字定义接口,同时接口成员有以下五种形式:① 对象属性 ② 对象的属性索引 ③ 对象方法 ④ 函数 ⑤ 构造函数-
对象属性
1
2
3
4
5
6
7
8/* 接口成员 - 对象属性
- 属性之间使用分号 ";" 或逗号 "," 分隔,最后一个属性结尾的分号或逗号可以省略
- 允许定义可选属性、只读属性 */
interface A {
name: string;
age?: number; // 可选属性
readonly isFullTime: boolean; // 只读属性
} -
对象的属性索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/* 接口成员 - 对象的属性索引
- 对象的属性索引的一般格式为 [indexName: indexType]: valueType,其中 indexType 表示属性名类型,可选 string、number、symbol
- 属性的数值索引,即 indexType 为 number,表示指定数组的类型,如接口 B 所示
- 接口中同时定义了属性的字符串索引和数值索引,则数值索引必须服从于字符串索引,如接口 C、D 所示。这是因为 JavaScript 中,数值属性名最终会自动转换为字符串属性名 */
interface B {
[key: number]: string;
}
const arr: B = ['a', 'b', 'c'];
interface C {
[key: number]: string;
[key: string]: string;
} // √
interface D {
[key: number]: number; // 这里应该修改为 string
[key: string]: string;
} // × -
对象方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38/* 接口成员 - 对象方法
- 对象方法一共有三种写法,如接口 E、F、G 所示
- 对象方法名可以采用表达式,如接口 H 所示
- 对象方法可以重载,interface 中定义的重载的函数只需要给出重载声明,而不需要给出重载实现,但在使用该接口时,需要给出对应重载的函数的重载声明和重载实现,如 I 所示 */
interface E {
f(x: string): string
};
interface F {
f: (x: string) => string;
};
interface G {
f: { (x: boolean): string }
};
const f = 'f';
interface H {
[f](x: string): string;
};
/* 只给出重载声明 */
interface I {
f(x: number, y: number): number;
f(x: string, y: string): string;
f(x: boolean, y: boolean): boolean;
};
/* 使用接口 I 时,定义的对象方法要给出重载声明 + 重载实现 */
function add(x: number, y: number): number;
function add(x: string, y: string): string;
function add(x: boolean, y: boolean): boolean;
function add(x: number | string | boolean, y: number | string | boolean): number | string | boolean {
if (typeof x === 'number' && typeof y === 'number') return x + y;
else if (typeof x === 'string' && typeof y === 'string') return x + y;
else if (typeof x === 'boolean' && typeof y === 'boolean') return x || y;
else throw new Error('worng parameters');
}
const obj: I = {
f: add
} -
函数
1
2
3
4
5
6/* 接口成员 - 函数
- 使用接口表示函数的语法如接口 J 所示 */
interface J {
(x: string): string;
}
const speak: J = (x) => { return x.toUpperCase() }; -
构造函数
1
2
3
4
5
6/* 接口成员 - 构造函数
- 使用接口表示构造函数的语法如接口 K 所示
- 在 TypeScript 中,构造函数特指具有 constructor 属性的类 */
interface K {
new(name: string, age: number): { name: string, age: number };
}
-
-
使用说明
-
与对象类似,TypeScript 允许通过属性/方法名读取对应属性/方法的类型,也就是对
interface
进行索引访问。1
2
3
4
5
6interface IStudent {
name: string;
age: number;
}
type Name = IStudent['name']; // string
-
1.2 接口继承
-
解释:接口可以使用
extends
关键字继承接口、对象类型、类身上的属性,从而避免书写重复的属性。 -
interface
继承interface
1
2
3
4interface A { x: string; }
interface B extends A { y: string; }
const obj: B = { x: "hello", y: "world" };-
接口允许多重继承,即允许有多个父接口
1
2
3
4
5interface A { x: string; }
interface B { y: string; }
interface C extends A, B { z: string }
const obj: C = { x: "hello", y: "world", z: "!" }; -
子接口与父接口的同名属性必须类型兼容,多个父接口之间的同名属性必须类型一致,否则会报错
1
2
3
4
5
6
7
8
9/* 父子接口的同名属性需兼容 */
interface A { x: string | number; }
interface B extends A { x: string; } // √
interface C extends A { x: boolean; } // ×
/* 父接口之间的同名属性需一致 */
interface M { x: string; }
interface N { x: number; }
interface P extends M, N { }; // ×
-
-
interface
继承type
1
2
3
4type A = { x: string; y: string; }
interface B extends A { z: string; }
const obj: B = { x: "Hello", y: "World", z: "!" }补充:
interface
继承type
的前提是,type
关键字定义的类型是对象。 -
interface
继承class
1
2
3
4
5
6
7class A {
x: string = '';
y(): boolean { return true; }
}
interface B extends A { z: number; }
const obj: B = { x: "Hello", y() { return true }, z: 1 };补充:当
class
定义的类中包含private
或protected
的成员时,interface
继承class
的意义不大!
1.3 接口合并
-
解释:在 TypeScript 中,同名接口会自动合并为一个接口。这一特性旨在兼容 JavaScript 中向全局对象或外部库扩展属性和方法的常见做法。通过定义同名接口,TypeScript 会自动将新增的属性和方法与原始接口合并,从而实现类型的无缝扩展。
1
2
3
4
5
6
7
8
9/* 接口合并 */
interface A { x: string; }
interface A { y: string; }
const obj: A = { x: "Hello", y: "World" };
/* 类型扩展,以下代码执行后,允许在 document 对象上添加自定义属性 x */
interface Document { x: string; }
document.x = "Hello"; -
使用说明
-
接口合并时,同名属性必须类型一致,否则会报错
1
2interface A { x: string; }
interface A { x: number; } // × -
接口合并时,同名方法可以有不同的类型声明,即方法重载,此时:后边定义的接口的方法声明优先级更高;包含字面量参数的方法声明优先级最高。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17interface B {
add(x: "hello"): void;
}
interface B {
add(x: boolean): boolean;
}
interface B {
add(x: boolean | number): string;
add(x: number): number;
}
// 上述代码等价于
interface B {
add(x: "hello"): void;
add(x: boolean | number): string;
add(x: number): number;
add(x: boolean): boolean;
} -
两个
interface
的联合类型中的同名属性也是联合类型1
2
3interface C { x: string; }
interface D { x: number };
type E = C | D; // string | number
-
1.4 interface Vs. type
-
相同点:
interface
和type
都可以定义对象类型。1
2
3
4
5
6
7
8
9type Country = {
name: string;
capital: string;
}
// 等价于
interface Country {
name: string;
capital: string;
} -
不同点
概述 interface
type
类型范围 对象类型(包括数组、函数等) 对象类型 + 非对象类型 继承方式 使用 extends
关键字继承接口、对象类型或类使用 &
运算符继承接口、对象类型合并允许 同名 interface
会自动合并(开放)同名 type
会报错(封闭)属性映射 不能包含属性映射 可以包含属性映射 this
允许允许使用 this
不允许使用 this
原始数据类型扩展 不允许扩展原始数据类型 允许扩展原始数据类型 复杂类型表示 不可以表示复杂类型 可以表示复杂类型(如交叉类型和联合类型) 补充:一般情况下,由于
interface
灵活度较高,建议优先使用~1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26/* 属性映射 */
interface Data {
x: number;
y: string;
}
type Copy1 = {
[key in keyof Data]: Data[key];
} // √,Copy1 是一种基于映射类型的方式来精确复制 Data 的结构。
interface Copy2 {
[key in keyof Data]: Data[key];
} // ×
/* 原始数据类型扩展 */
interface MyStr1 extends string {
type: "new";
} // ×,An interface cannot extend a primitive type like 'string'. It can only extend other named object types.(2840)
type MyStr2 = string & {
type: "new";
} // √
const str: MyStr2 = new String("hello") as MyStr2;
str.type = "new"
console.log(str, str.type) // String: "hello", "new"
2. 类
2.1 基本使用
-
解释:类是面向对象编程的核心构建单元,它通过封装属性(数据)和方法(行为)来实现对现实世界实体的抽象和建模。TypeScript 中通过
class
关键字定义类。 -
基本语法:
class 类名 { 属性; 方法; /* ... */ }
补充:TypeScript 中使用
class
关键字定义类,同时类成员有以下五种形式:① 属性 ② 只读属性 ③ 方法 ④ 访问器方法 ⑤ 属性索引-
属性:可以在类顶部声明属性及其类型。在未指定属性类型的情况下,如果属性被赋初值,TypeScript 会自动推断该属性类型,否则会认为该属性类型为
any
。一般情况下,编译选项--strictPropertyInitialization
被打开,此时 TypeScript 会检查属性是否设置了初值,没有就报错(如果在构造函数中进行了赋值,也不会报错)。为了避免因为属性未赋值而产生的报错,可以使用非空断言"!"
,表示对应的属性肯定不会为空。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Point {
x: number = 10; // 标准写法
y = 10; // 自动推断属性 y 为 number 类型
z; // 认为属性 z 为 any 类型
isValid!: boolean; // 非空断言,表示属性 isValid 一定不会为 null 或 undefined
isOrigin: boolean; // 因为在构造函数中对 isOrigin 进行了赋值,所以这里不会报错
constructor(x: number, y: number, z: number, isValid: boolean, isOrigin: boolean) {
this.x = x;
this.y = y;
this.z = z;
this.isValid = isValid;
this.isOrigin = isOrigin;
}
}非空断言(Non-null Assertion):TypeScript 中通过在表达式末尾添加一个感叹号
"!"
实现非空断言,告诉编译器该表达式的值绝对不是null
或undefined
。 -
只读属性:属性名前使用
readonly
修饰符以表示该属性是只读属性。可以在类顶部给只读属性赋值,也可以在构造函数中给只读属性赋值,如果两处同时赋值,以构造函数中的为准。1
2
3
4
5
6
7
8
9
10
11class Point {
readonly x: number = 10;
readonly y: number;
readonly z: number = 10;
constructor() {
this.y = 20;
this.z = 30;
/* 此时属性为 x = 10, y = 20, z = 30 */
}
} -
方法:声明方法的语法为
methodName(paraName1: paraType1, /* ... */) {}
,其中可以省略返回值类型,让 TypeScript 自行推断。方法的参数与普通函数的参数一样,可以使用参数默认值。同时,方法也可以进行重载(包括构造方法)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36class Point {
readonly x: number;
readonly y: number;
/* 参数默认值 */
add(point: Point = new Point(1, 1)) {
return new Point(
this.x + point.x,
this.y + point.y
)
}
/* 方法重载 */
mul(times: number): Point;
mul(point: Point): Point;
mul(para: number | Point): Point {
if (typeof para === 'number') {
return new Point(
this.x * para,
this.y * para
);
} else if (para instanceof Point) {
return new Point(
this.x * para.x,
this.y * para.y
);
} else {
throw new Error("Invalid parameter type. Expected number or Point.");
}
}
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}补充:构造方法不能声明返回值类型,因为其总是返回实例对象。
-
getter / setter
(存取器方法):存取器(accessor)是特殊的方法,包括取值器(getter)和存值器(setter),其中取值器用于读取属性,存值器用于写入属性(这里读取或写入的属性称之为访问器属性)。一般来说,存取器对类中的某个属性进行代理,从而对其读取和写入操作添加更加更加详细的控制。取值器语法为get 属性名(){ return 被代理属性 }
,存值器语法为set 属性名(value: valueType){ 被代理属性 = value }
。1
2
3
4
5
6
7
8
9
10
11class Observer {
_data: { [key: string]: any } = {};
get data() {
return this._data;
}
set data(value: { [key: string]: any }) {
this._data = value
}
}补充-1:如果一个访问器属性只有
getter
,那么该属性就是只读属性。补充-2:TypeScript 5.1 之前要求
getter
的返回值型必须兼容setter
的参数类型。补充-3:访问器属性的
getter
和setter
的可访问性必须一致!要么都公开,要么都私有! -
属性索引:在 TypeScript 中,类允许定义属性索引(如字符串、数字或
Symbol
),用于约束对应属性值的类型。方法和存取器被视为特殊的字符串属性索引。例如,[s: string]: boolean | ((s: string) => boolean)
约束了类中所有以字符串为属性名的属性:这些属性的值必须是布尔值,方法必须是返回布尔值的函数,而存取器必须返回布尔值。一旦不满足约束,就会报错。
-
-
静态成员:类的内部能够使用
static
关键字来定义静态成员(包括属性或方法)。静态成员只能通过类本身来使用,并且可以使用public
、private
、protected
修饰符来修饰静态成员。
2.2 类的实现
-
解释:TypeScript 中,可以通过
interface
或type
以对象的形式对类的成员进行约束,类可以使用implements
关键字应用这些约束,称之为类的实现。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15interface Person {
name: string;
age: number;
}
class Chinese implements Person {
name: string;
age: number;
city: string = "Bei'jing";
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}补充-1:
interface
定义的类成员约束与类自身的类型声明并不等同,所以类在符合interface
所提供的约束条件时,还需补充相应的类型声明。补充-2:类可以定义
interface
中没有声明的属性和方法。补充-3:如果
implements
关键字后边是一个类,此时这个类被当做接口使用。 -
多重实现:在 TypeScript 中,类能够实现多个接口,这意味着它可以接受来自多个接口的约束。当类实现多个接口时,接口之间用逗号分隔。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17interface A {
x: string;
}
interface B {
y: number;
}
interface C {
z: boolean;
}
class MyClass implements A, B, C {
x: string = '1';
y: number = 1;
z: boolean = true;
}补充-1:若一个类同时实现多个接口,可能会导致代码管理变得困难,此时可借助类的继承或者接口的继承来对这种情况加以改写。
补充-2:当一个类实现多个接口时,各接口之间不能存在会引发冲突的属性。
-
类的合并:TypeScript 中不允许出现两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类的定义。
1
2
3
4
5
6
7
8
9
10interface Car {
isNew: boolean;
}
class Car {
price: number = 10000;
}
const car = new Car();
car.isNew = true; // 如果这里不给 isNew 属性赋值,则默认为 undefined
2.3 类的类型
-
实例类型:在 TypeScript 里,类(名)本身就属于一种类型,它代表的是该类的实例类型,而非类自身的类型。
1
2
3
4
5
6
7
8
9
10
11
12class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const p1: Point = new Point(1, 2); // 这里 Point 被当做实例类型使用
const p2: Point = { x: 1, y: 2 }; // 这里 Point 被当做对象类型使用补充-1:若类 A 实现了接口 B,在声明引用类 A 实例的变量时,既可将变量类型声明为 A,也能声明为 B。二者唯一的区别在于,当声明类型为 B 时,该变量无法访问类 A 中定义的那些属性和方法。
补充-2:在 TypeScript 中,存在三种可充当对象类型的方式,分别是
type
、interface
以及class
。 -
自身类型:可通过以下三种方式获得一个类的自身类型。
-
方式一
typeof 类名
-
方式二
new (paraName1: paraType1, paraName2: paraType2) => 类名
补充:方式二的合理性在于,在 JavaScript 中,类是构造函数的语法糖,即构造函数的另一种写法,因此类的自身类型可以写成构造函数的形式。
-
方式三
{ new (paraName1: paraType1, paraName2: paraType2): 类名 }
补充:可以参照方式三将类的自身类型提炼为一个
interface
,便于使用。
-
-
结构类型
-
如果一个对象满足类的实例结构,便认为此对象和该类是同一类型。
1
2
3
4
5
6
7class Person {
name: string = "Jack";
age: number = 19;
static isAdult: boolean = true;
}
const p: Person = { name: "Tom", age: 41 };补充-1:确定对象和类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法。
补充-2:当对象和类属于同一类型时,不可以使用
对象 instanceof 类名
的方式判断某个对象是否跟某个类属于同一类型。 -
如果类 A 具备类 B 的全部结构,即便类 A 还有额外的属性和方法,也会认为类 A 是兼容类 B 的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Person {
name: string = "Jack";
age: number = 19;
static isAdult: boolean = true;
}
class Student {
name: string = "Tom";
age: number = 89;
grade: number = 7;
static isAdolescent: boolean = true;
}
const stu: Person = new Student();补充-1:确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法。
补充-2:若类中包含私有成员或保护成员,在判定两个类的兼容关系时,需保证这些私有或保护成员源自同一个类,也就是说,这两个类之间得存在继承关系。
-
2.4 类的继承
-
解释:一个类(子类)可以使用
extends
关键字继承另一个类(父类、基类)的所有属性和方法。1
2
3
4
5
6
7
8class Creature {
eat() { console.log("eat food...") }
}
class Cat extends Creature { }
const cat = new Cat();
cat.eat(); // "eat food..." -
使用说明
-
依照结构类型原则,因为子类涵盖了父类的全部结构,子类兼容父类。
-
子类能够对父类里的同名方法进行重写,但其类型定义务必要兼容父类中该同名方法的类型定义。
1
2
3
4
5
6
7
8
9
10
11
12
13class Creature {
eat() { console.log("eat food...") }
}
class Cat extends Creature {
eat(n?: number) {
if (n === undefined) super.eat();
else console.log(`eat ${n} fish...`);
}
}
const cat = new Cat();
cat.eat(12);补充-1:当子类重写父类的方法时,若在重写的方法前添加
override
关键字,TypeScript 便会检查父类中是否存在对应的同名方法,若不存在,就会报错。补充-2:在 TypeScript 编译过程中,若开启了
noImplicitOverride
编译选项,那么当子类重写父类里的同名方法时,要是不添加override
关键字,就会出现报错。 -
在子类中,能够将父类的保护成员的可访问性修改为公开,但不能把它设为私有。
1
2
3
4
5
6
7
8
9
10
11class A {
protected x = 1;
protected y = 2;
protected z = 3;
}
class B extends A {
public x = 1; // √
private y = 2; // ×
protected z = 3; // √
} -
extends
关键字后不一定是类名,可以是一个类型为构造函数的表达式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33class Bike {
speed = "slow";
drive() { console.log("Bike rides slowly") }
}
class Car {
speed = "fast";
drive() { console.log("Car drives quickly") }
}
interface Transportation {
speed: string;
drive(): void;
}
interface TransportationConstructor {
new(): Transportation;
}
function getTransportation(): TransportationConstructor {
return Math.random() > 0.5 ? Bike : Car;
}
/* getTransportation() 是一个表达式,执行返回 Bike 或 Car */
class Vehicle extends getTransportation() {
sayInfo() {
console.log(`The speed is ${this.speed}`);
this.drive();
}
}
const v = new Vehicle();
v.sayInfo();
-
2.5 可访问性修饰符
类的内部成员的外部可访问性由三个可访问性修饰符(access modifiers)控制:public
、private
和 protected
。
可访问性修饰符 | 解释 | 外部(实例)可访问 | 类内部可访问 | 子类可访问 |
---|---|---|---|---|
public |
公开成员(默认修饰符) | √ | √ | √ |
private |
私有成员 | × | √ | × |
protected |
保护成员 | × | √ | √ |
-
private
-
子类中不能定义父类私有成员的同名成员。
1
2
3
4
5
6
7
8
9
10
11class Creature {
private name: string = "creature";
private age: number = 0;
private gender: string = "male";
}
class Dog extends Creature {
public name: string = "dog"; // ×
protected age: number = 0; // ×
private gender: string = "male"; // ×
} -
private
修饰符定义的私有成员并不是真正意义上的私有成员,实例对象可以通过方括号"[]"
或in
运算符访问对应的私有成员。因此,更推荐使用 ES2022 引入的私有成员写法#propName
,即真正意义上的私有成员。1
2
3
4
5
6
7
8
9
10
11
12
13
14class Creature {
private name: string = "creature";
#age: number = 0;
}
const c = new Creature();
c.name; // ×
c['name']; // "creature"
'name' in c; // true
c.age; // ×
c['age']; // ×
'age' in c; // false -
构造函数设为私有后,不能用
new
操作符生成实例对象,只能在类内创建。类内创建实例的静态方法叫工厂函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Creature {
name: string;
age: number;
private static instance?: Creature; // Creature 类的唯一实例
private constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
/* 工厂函数,用于获取 Creature 类实例(单例) */
static getInstance() {
if (!Creature.instance)
Creature.instance = new Creature("lucky", 12);
return Creature.instance;
}
}
-
-
protected
-
子类中可以定义父类保护成员的同名成员。
1
2
3
4
5
6
7
8
9
10
11class Creature {
protected name: string = "creature";
protected age: number = 0;
protected gender: string = "male";
}
class Dog extends Creature {
public name: string = "dog"; // √
protected age: number = 0; // √
private gender: string = "male"; // ×
}
-
-
实例属性的简写形式:在 TypeScript 中,支持将以下代码
(1)
简写为代码(2)
,即可访问性修饰符public
、private
、protected
以及只读修饰符readonly
移动到构造函数中去。补充:
readonly
可以和public
、private
、protected
混合使用。1
2
3
4
5
6
7
8
9
10// (1)
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}1
2
3
4// (2)
class Point {
constructor(public x: number, public y: number) { }
}
2.6 顶层属性的初始化问题
-
背景:对于类的顶层属性,在早期 TypeScript 中,① 在类顶层声明属性,② 运行构造方法,同时完成初始化;在 ES2022 及以后,① 在类顶层声明属性,同时完成初始化,② 运行构造方法。因此,同一段代码在 TypeScript 和 JavaScript 中的运行结果可能会有所不同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14// TypeScript 顶层属性在构造方法中初始化
class Point {
x: number = 1;
y: number = 2;
constructor(x: number, y: number) {
// 这里隐含了代码
// this.x = 1;
// this.y = 2;
console.log(this.x, this.y);
this.x = x;
this.y = y;
}
}1
2
3
4
5
6
7
8
9
10
11// JavaScript 顶层属性在类顶层初始化
class Point {
x = 1;
y = 2;
constructor(x, y) {
console.log(this.x, this.y);
this.x = x;
this.y = y;
}
} -
问题:因为顶层属性初始化位置不一致而导致代码运行结果不一致的情况通常发生在以下两种情形。
-
顶层属性的初始化依赖于其他实例属性。
1
2
3
4
5
6
7
8
9
10
11
12
13// TypeScript
class User {
age = this.currentYear - 1998;
constructor(private currentYear: number) {
// 这里隐含了
// this.currentYear = currentYear;
// this.age = this.currentYear - 1998;
console.log('Current age:', this.age);
}
}
const user = new User(2023); // 251
2
3
4
5
6
7
8
9
10// JavaScript
class User {
age = this.currentYear - 1998; // undefined - 1998 = NaN
constructor(currentYear) {
console.log('Current age:', this.age);
}
}
const user = new User(2023); // NaN -
子类声明的顶层属性在父类完成初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38// TypeScript
interface Animal {
animalStuff: any;
}
interface Dog extends Animal {
dogStuff: any;
}
class AnimalHouse {
resident: Animal;
constructor(animal: Animal) {
// 这里隐含了
// this.resident = undefined
this.resident = animal;
}
}
class DogHouse extends AnimalHouse {
resident: Dog;
constructor(dog: Dog) {
// 这里隐含了
// this.resident = undefined
super(dog);
}
}
const dog = {
animalStuff: 'animal',
dogStuff: 'dog'
};
const dogHouse = new DogHouse(dog);
console.log(dogHouse.resident);
/*
{
"animalStuff": "animal",
"dogStuff": "dog"
}
*/1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// JavaScript
class AnimalHouse {
resident;
constructor(animal) {
this.resident = animal;
}
}
class DogHouse extends AnimalHouse {
resident;
constructor(dog) {
super(dog);
}
}
const dog = {
animalStuff: 'animal',
dogStuff: 'dog'
};
const dogHouse = new DogHouse(dog);
console.log(dogHouse.resident); // undefined
-
-
解决方式
- 使用 TypeScript 3.7 开始提供的编译选项
useDefineForClassFields
,该编译选项为true
,则表示采用 ES2022 标准的处理方法,否则采用 TypeScript 早期的处理方法。编译选项useDefineForClassFields
的默认值与编译选项target
有关,如果target
的取值为 ES2022 或更高,那么useDefineForClassFields
的取值为true
,否则为false
。 - 将所有顶层属性的初始化都放到构造方法中。
- 对于类的继承导致的问题,可以使用
declare
命令,去声明子类的顶层属性的初始化由父类实现。
- 使用 TypeScript 3.7 开始提供的编译选项
2.7 抽象类
抽象类:一种特殊的类,其类名前需加上 abstract
关键字。抽象类是其他类的模板,用于定义一些共有的接口,不能进行实例化。
1 | abstract class Point { |
- 抽象类包括抽象成员和非抽象成员,抽象成员是未实现的属性和方法,使用
abstract
关键字修饰,需要在非抽象子类中实现;非抽象成员是已实现好的属性和方法。 - 抽象类的子类也可以是抽象类。
- 抽象成员不能被
private
修饰符修饰。
2.8 this 之用
类的方法中使用的 this
表示该方法当前所在的对象。
1 | class A { |
-
TypeScript 允许函数增添一个名为
this
的参数,位于参数列表的首位,用于描述函数内部this
的类型。在编译时,TypeScript 会对函数进行检查,如果函数参数列表的第一个参数名为this
,那么编译的结果将会删除这个参数。 -
TypeScript 中提供编译选项
noImplicitThis
,打开这个编译选项后,如果this
的值被推断为any
类型就会报错。 -
在类内部,
this
可以当作类型(返回值)来使用,表示当前类的实例对象。如果某个方法的返回值是一个布尔值,用于表明this
是否属于某种类型,此时可以将this is Type
作为方法的返回值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class MyArray<T> {
constructor(public arr: T[]) { }
reverse(): this {
this.arr.reverse();
return this;
}
slice(a: number, b: number): this {
this.arr = this.arr.slice(a, b);
return this;
}
isMyArray(): this is MyArray<T> {
return this instanceof MyArray;
}
}
const arr = new MyArray([1, 2, 3, 4, 5]);
console.log(arr.reverse());
console.log(arr.slice(0, 2));
console.log(arr.isMyArray());
TS - 类型系统 5(泛型、Enum)
1. 泛型
1.1 基本使用
-
解释:泛型(Generics)是一种特殊的编程逻辑,它通过引入类型参数(type parameter)来建立输入类型和输出类型之间的关联。在 TypeScript 中,泛型应用于函数、接口、类和类型别名中,类型参数放置在尖括号
<>
中。类型参数必须是合法的标识符,常用 T、U、V 等字母命名,各个类型参数之间使用逗号,
分隔。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21function getRandomElement<T>(arr: T[]): T {
const randomIndex = Math.floor(Math.random() * arr.length);
return arr[randomIndex];
}
console.log(getRandomElement<string>(['a', 'b', 'c']));
console.log(getRandomElement(['a', 'b', 'c'])); // 有时可以省略类型参数的值,让 TypeScript 自行推断
function combine<T>(arr1: T[], arr2: T[]): T[] {
return arr1.concat(arr2);
}
console.log(combine<number | string>([1, 2, 3], ['a', 'b', 'c'])); // 有时类型过于复杂,TypeScript 无法推断,此时需要显示给出类型参数的值
function map<T, U>(arr: T[], f: (arg: T) => U): U[] {
return arr.map(f);
}
console.log(map<string, number>(['1', '2', '3'], (x) => parseInt(x))); // 可使用多个类型参数 -
语法
-
函数
1
2
3
4
5
6
7
8
9
10// 写法一:函数声明
function func1<T>(msg: T): T {
return msg;
}
// 写法二:函数类型 - 一般写法
let func2: <T>(arg: T) => T = func1;
// 写法三:类型声明 - 对象写法
let func3: { <T>(arg:T): T } = func1; -
接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 情形一:接口定义属性类型
interface Info<T> {
contents: T;
}
// 情形二:接口定义方法或函数类型
// 2-1:类型参数定义在接口,实现该接口时需要给出类型参数的值。
interface Comparator<T> {
compareTo(value: T): number;
}
// 2-2:类型参数定义在方法或函数中,实现该接口时不需要给出类型参数的值,使用对应方法或函数时才需要给出类型参数的值。
interface Fn {
<T>(arg: T): T;
} -
类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 写法一:一般写法
class Pair<K, V> {
key: K;
value: V;
}
// 写法二:类表达式
// 补充:类表达式包含匿名类表达式和具名类表达式,后者更有利于调试,这里使用了匿名类表达式作为示例
const Person = class <T> {
constructor(public name: string, public age: number, public info: T) { }
}
// 写法三:构造函数类型写法
// 解释:JavaScript 中的类本质上是一个构造函数
type MyClass1<T> = new (...args: any[]) => T;
interface Myclass2<T> {
new(...args: any[]): T;
}
/* 注意事项
- 泛型类描述的是类的实例,因此类型参数不可应用于类中的静态属性和静态方法
*/ -
类型别名
1
2
3
4
5
6
7
8
9
10
11
12// 示例一:定义联合类型
type Nullable<T> = T | undefined | null;
// 示例二:定义对象类型
type Container<T> = { value: T };
// 示例三:定义二叉树类型
type Tree<T> = {
value: T;
left: Tree<T> | null;
right: Tree<T> | null;
}
-
-
使用说明
-
类型参数至少需要出现两次,否则可能是非必要的。
-
泛型允许嵌套,即类型参数可以是另一个泛型。
1
2
3type OrNull<T> = T | null;
type OneOrMany<T> = T | T[];
type OneOrManyOrNull<T> = OrNull<OneOrMany<T>>; // 等价于 T | T[] | null
-
1.2 可选的类型参数
解释:在TypeScript中,类型参数可以设置默认值。
- 当使用泛型但未指定类型参数时,将使用这个默认值。然而,如果 TypeScript 能够推断出类型参数的值,那么推断出的值将覆盖默认值。
- 类型参数的默认值在类中非常常见。
- 设置了默认值的类型参数也被称为可选类型参数。在多个类型参数列表中,可选类型参数必须位于最后。
1 | function getFirst<T = string>(arr: T[]): T { |
1.3 类型参数的约束条件
解释:在 TypeScript 中,可以为类型参数设置约束条件,语法为 <TypeParameter extends ConstraintType>
。这表示类型参数TypeParameter
必须是约束类型 ConstraintType
的子类型。
- 同时,类型参数可以设置默认值和约束条件,但默认值必须满足约束条件。
- 也可以使用其他类型参数作为约束条件,如
<T, U extends T>
。然而,类型参数不能以自身作为约束条件,如<T extends T>
是不允许的。
1 | function comp<T extends { length: number }>(a: T, b: T) { // 这里约束类型参数 T 必须包含 number 类型的属性 length |
2. Enum
1.1 基本使用
-
解释:Enum 是 TypeScript 新增的一种数据结构和类型,译为枚举,用于集中管理一组有关系的常量,从而增加代码的可读性和可维护性。枚举通过
enum
关键字定义,分为数值枚举、const
枚举和字符串枚举。补充-1:可以通过点运算符或者方括号运算符来访问枚举中的某个成员。
补充-2:枚举既是一种类型,也是一个值。编译之后的枚举是一个 JavaScript 对象。
补充-3:由于枚举编译后是一个对象,因此建议谨慎使用枚举。一般来说,枚举可以被对象的
as const
断言所取代。同时,不能存在与枚举同名的变量(包括对象、函数、类等)。 -
数值 Enum:数值 Enum 即枚举成员的值为数值的枚举,如果枚举成员未赋值,每个成员的值默认从 0 开始有序递增,如 0、1、2、……
1
2
3
4
5
6
7
8
9
10
11
12
13
14enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3,枚举成员的值默认是数值,且从 0 开始有序递增
}
console.log(Direction.Up); // 0,点运算符访问枚举成员
console.log(Direction['Down']); // 1,方括号运算符访问枚举成员
const up1: number = Direction.Up;
const up2: Direction = Direction.Up; // 枚举不仅仅是一个值,也是一种类型
const Direction = 1; // ×,不能有与枚举同名的变量补充-1:数值 Enum 支持为成员显式赋数值,但不能是大整数。其中,成员的值可以相同。
补充-2:数值 Enum 中,如果只设定某一个成员的值,后续成员的值就会以这个值为基准有序递增,如 N、N + 1、N + 2、……
补充-3:数值 Enum 中成员的值可以是表达式。
补充-4:数值 Enum 中成员的值都是只读的,无法重新赋值。
-
const Enum:const Enum 即特殊的 Enum,其与一般 Enum 的区别在于 ① const Enum 使用
const enum
进行定义;② TypeScript 不会将 const Enum 编译为一个对象,而是会将所有 Enum 成员出现的场合替换为对应成员的值,有利于提高性能。1
2
3
4
5
6
7
8
9
10
11
12const enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
Direction.Up; // 被编译为 0 /* Direction.Up */;
Direction.Down; // 被编译为 1 /* Direction.Down */;
Direction.Left; // 被编译为 2 /* Direction.Left */;
Direction.Right; // 被编译为 3 /* Direction.Right */;
Direction; // ×,因为 Direction 没有编译产物,TypeScript 只会把代码中 Direction 成员替换为对应的值补充:如果希望 const Enum 编译产物仍为对象,那么就要打开编译选项
preserveConstEnum
。 -
字符串 Enum:字符串 Enum 即枚举成员的值为字符串的枚举,与数值 Enum 不同,字符串 Enum 成员值必须显式设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14enum Direction {
Up, // 0
Down, // 1
Left = "Left",
Right = "Right"
}
let d1 = Direction.Up;
let d2 = Direction.Left;
d1 = 1; // 可以赋值数值,但是只能赋值 0 或 1(枚举中已有的成员的值)
d2 = "Left"; // 不可以赋值字符串,即使赋值 "Left" 或 "Right"(枚举中已有的成员的值)
let d3: Direction = Direction.Left;
let d4: "Left" | "Right" = d3; // 字符串 Enum 可以使用联合类型替代补充-1:字符串 Enum 允许成员值包含数值和字符串,但是在字符串 Enum 中,如果设置了某个成员值为字符串,那么其后的成员都需要设置为字符串,其前的成员的值默认从 0 开始有序递增。
补充-2:Enum 的成员值只能是数值和字符串。
补充-3:如果一个变量类型是数值 Enum,那么还可以将其他数值赋值给该变量;如果一个变量类型是字符串 Enum,那么不能将其他字符串赋值给该变量。
补充-4:字符串 Enum 做类型用时,可以使用联合类型来替代。
补充-5:字符串 Enum 成员值不能使用表达式进行赋值。
1.2 Enum 合并
-
解释:多个同名的 Enum 会自动合并。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18enum Color {
red = 'red',
green = 'green'
}
enum Color {
yellow = 'yellow',
blue = 'blue'
}
/* 等价于 */
enum Color {
red = 'red',
green = 'green',
yellow = 'yellow',
blue = 'blue'
} -
使用说明
- 多个 Enum 合并时,只允许其中一个的首个成员省略显式赋值,否则会报错。
- 多个 Enum 合并时,不允许有同名成员,否则报错。
- 多个 Enum 合并时,要么都是 const Enum,要么都是非 const Enum。
1.3 反向映射
解释:反向映射指的是能通过成员值获取成员名,不过只有数值 Enum
才有反向映射,这是因为 TypeScript 对不同Enum
的编译结果不一样造成的。
-
数值 Enum 的编译结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36enum Direction {
Up,
Down,
Left,
Right
}
/* 上述代码的编译结果为, */
/*
"use strict";
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
*/
/* 上述编译结果等价于, */
/*
{
"0": "Up",
"1": "Down",
"2": "Left",
"3": "Right",
"Up": 0,
"Down": 1,
"Left": 2,
"Right": 3
}
*/ -
字符串 Enum 的编译结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32enum Direction {
Up = "Up",
Down = "Down",
Left = "Left",
Right = "Right"
}
/* 上述代码的编译结果为, */
/*
"use strict";
var Direction;
(function (Direction) {
Direction["Up"] = "Up";
Direction["Down"] = "Down";
Direction["Left"] = "Left";
Direction["Right"] = "Right";
})(Direction || (Direction = {}));
*/
/* 上述编译结果等价于, */
/*
{
"Up": "Up",
"Down": "Down",
"Left": "Left",
"Right": "Right"
}
*/
1.4 Enum 提取
-
成员名提取:TypeScript 中,可通过
keyof typeof 枚举名
这种方式,提取指定枚举的全部成员名,并将其作为联合类型返回。1
2
3
4
5
6
7
8enum Direction {
Up = "up",
Down = "down",
Left = "left",
Right = "right"
}
type D = keyof typeof Direction; // "Up" | "Down" | "Left" | "Right" -
成员值提取:在 TypeScript 中,可通过
in
运算符提取指定枚举的全部成员值。1
2
3
4
5
6
7
8enum Direction {
Up = "up",
Down = "down",
Left = "left",
Right = "right"
}
type D = { [key in Direction]: any }; // { up: any; down: any; left: any; right: any; }
TS - 类型断言
基本使用
-
类型断言:TypeScript 支持类型断言,允许开发者在代码中指定某个值的类型,此时编译器会放弃对该值的类型推断。
-
语法
<Type>value
(不推荐,因为与 TypeScript 支持的 React-JSX 语法冲突)value as Type
(推荐)
-
使用说明
-
可以使用类型断言解决对象类型的严格字面量检查报错。
1
const p: { x: number } = { x: 0, y: 0 } as { x: number, y: number }; // ✅
-
可以使用类型断言指定
unknown
类型的变量的具体类型。1
2const str1: unknown = 'Hello World';
const str2: string = str1 as string; // ✅ -
类型断言必须满足 “实际类型是断言类型的子类型” 或 “断言类型是实际类型的子类型”。但是该条件可以通过 “先将指定值断言为
any
或unknown
类型,再将其断言为目标类型” 来避免。1
2
3const n = 1;
const m:string = n as string; // ❌
const k:string = n as any as string; // ✅
-
as const 断言
-
as const
断言:这种断言只能应用于字面量,它将字面量的类型断言为不可变类型,进一步缩小为 TypeScript 允许的最具体的类型。 -
语法
<const>value
value as const
-
使用说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 1. as const 断言将某个值推断为值类型,此时 let 变量不能再改变值
let s = 'javascript' as const; // 类型为 'javascript'
// 2. as const 断言不能用于变量和表达式
let s1 = s as const; // ❌
let s2 = ('java' + 'script') as const; // ❌
// 3. as const 断言可以应用于整个对象(只读类型),也可以应用于对象中的单个属性(值类型,不可变)。
const p1 = { name: "Jack", age: 12 as const }; // 类型为 { name: string, age: 12 }
const p2 = { name: "Jack", age: 12 } as const; // { readonly name: "Jack", readonly age: 12 }
// 4. as const 断言应用于数组,将其断言为只读元组。(元组长度可定,数组长度不可定,因此可以使用扩展运算符 + 元组为函数传参)
const tuple = [1, 2, 3] as const; // 类型为 readonly [1, 2, 3]
// 5. as const 断言应用于枚举成员,将其断言为枚举的指定成员,此时 let 变量不能再改变为其他枚举成员
enum Directions { Up, Down, Left, Right }
let d1 = Directions.Down; // 类型为 Directions
let d2 = Directions.Down as const; // 类型为 Directions.Down
非空断言
-
非空断言:这种断言用于断言某些可能为空(可能为
undefined
或null
)的变量不会为空,避免编译报错。1
2
3
4
5const root = document.getElementById('root');
root!.addEventListener('click', e => {
/* ... */
}) -
语法:
变量!
-
使用说明
- 非空断言需要开发者确保一个表达式的值不为空。更保险的方式是,使用可能为空的变量前先进行手动检查。
- 非空断言只有在打开编译选项
strictNullChecks
时才有意义,否则编译器不会检查某个变量是否可能为空(即undefined
或null
)。
断言函数
-
断言函数:该函数是一种特殊函数,用于保证函数参数符合某种类型。如果函数参数符合指定类型,则不进行任何操作;否则,就会抛出错误,中断程序执行。
- 断言函数命名为
isType
,要断言的参数value
的类型为unknown
,函数体中包含断言逻辑。 - 断言函数有两种写法,
- 旧写法的返回值类型为
void
- 新写法的返回值类型为
asserts value is type
(相当于void
类型,asserts
和is
都是关键字,value
时要断言的函数参数名,type
时函数参数的预期类型)
- 旧写法的返回值类型为
- 断言函数命名为
-
语法
-
旧写法
1
2
3
4function isString(value: unknown): void {
if (typeof value !== 'string')
throw new Error('Not a string');
} -
新写法(推荐,更清晰地表达函数意图,即该函数为断言函数)
1
2
3
4function isString(value: unknown): asserts value is string {
if (typeof value !== 'string')
throw new Error('Not a string');
}
-
-
使用说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35// 1. 如果断言函数内部的检查与预期类型不一致,TypeScript 不会报错。
function isString1(value: unknown): asserts value is string {
if (typeof value !== 'number')
throw new Error('Not a number');
}
// 2. 断言函数的 asserts 语句等同于 void 类型,所以如果返回 undefined 或 null 以外的值,TypeScript 就会报错。
function isString2(value: unknown): asserts value is string {
if (typeof value !== 'string')
throw new Error('Not a string');
return true; // ❌
}
// 3. 可以借助工具类型 NonNullable<T> 来断言函数参数非空。
// 补充:NonNullable<T> 对应类型 T 去除空类型(undefined 或 null)后的剩余类型。
function assertsIsDefined<T>(value: T): asserts value is NonNullable<T> {
if (value === undefined || value === null)
throw new Error(`${value} is not defined`);
}
let x = null;
assertsIsDefined(x); // 此时代码执行时会抛出错误
// 4. 与断言函数不同,类型保护函数也可用于检查函数参数是否符合某种类型,但是其返回值为布尔值。
// 补充:类型保护函数命名为 isType,要保护的参数 value 的类型为 unknown,返回值类型为 value is type(相当于 boolean 类型),函数体中包含检查逻辑。
function isString3(value: unknown): value is string {
return typeof value === 'string';
}
// 5. 可以使用断言函数的简写形式(asserts value)来断言某个参数为真(即不等于 false,undefind,null)。
// 补充:断言函数的简写形式通常也可用来检查某个操作是否成功。
function assert(value: unknown): asserts value {
if (!value)
throw new Error(`${value} should be a truthy value.`)
}
ES6 - 模块
注意:这里所说接口指的是模块向外暴露的变量、类、函数。
简要概述
-
ES6 模块化与 CommonJS 和 AMD 模块化的区别
- ES6 模块化在编译时加载模块,其模块是通过
export
命令显式输出的代码,可以直接加载指定的接口。由于 ES6 模块在编译时加载,因此可以对代码进行静态分析,如引入宏和类型检验等。 - CommonJS 和 AMD 模块化在运行时加载模块,其模块是一个对象,必须加载完整个对象后,再从该对象身上读取指定的接口。
- ES6 模块化在编译时加载模块,其模块是通过
-
ES6 模块的特点
-
ES6 使用
export
命令导出模块的接口,import
命令导入其他模块提供的接口。 -
ES6 中一个文件就是一个模块,该文件中的所有接口,外部都无法获取(除非使用
export
命令导出接口)。 -
ES6 模块自动采用严格模式,即默认在模块头部添加
"use strict"
。补充:严格模式在 ES5 引入。
-
ES6 模块的顶层作用域中
this
指向undefined
,因此不应该在顶层作用域中使用this
。
-
模块语法
具名导出与导入
-
具名导出
-
语法
-
写法一:声明时导出
1
2
3export var day = 1;
export const month = 1;
export let year = 2025; -
写法二:先声明,再导出(推荐,因为可以写在脚本尾部,更清晰)
1
2
3
4
5var day = 1;
const month = 1;
let year = 2025;
export { day, month, year } -
写法三:先声明,再导出,同时设置别名(允许给一个变量指定多个别名)
1
2
3
4
5
6export {
day as iday,
month as imonth,
year as iyear,
year as jyear
}
-
-
使用说明
- 与 CommonJS 不同,ES6 通过
export
命令导出的接口与其对应的值是动态绑定的,即可以通过该接口访问到模块内部实时的值。 export
命令可以出现在模块中的任何位置,但一定要处于顶层作用域。
- 与 CommonJS 不同,ES6 通过
-
-
具名导入
-
语法
-
写法一:直接导入接口
1
import { day, month, year } from './xxx.js'
-
写法二:直接导入接口,同时设置别名
1
import { day as iday, month as imonth, year as iyear } from './xxx.js'
-
-
使用说明
-
import
命令导入的接口是只读的。虽然可以修改接口的属性,但是千万不要这么做,因为其他模块导入该接口时,会访问到被改写后的属性! -
from
命令用于指定模块的位置,其可以是相对路径、绝对路径或模块名。如果使用模块名,则必须包含配置文件,以告诉 JavaScript 引擎该模块的位置。 -
import
命令会被提升到整个模块顶部首先执行。 -
import
命令会执行所加载的模块,因此允许直接使用import '模块位置'
的方式执行模块。 -
多次加载一个模块只会执行一次,可以理解为
import
语句是 singleton 模式的。1
2
3
4
5import 'lodash';
import 'lodash';
// 等价于
import 'lodash';1
2
3
4
5import { foo } from 'my_module';
import { bar } from 'my_module';
// 等价于
import { foo, bar } from 'my_module'; -
虽然 Babel 允许在同一模块中混用 CommonJS 的
require
和 ES6 的import
,但不推荐这样做。因为 ES6 模块(静态加载)总是早于 CommonJS 模块(动态加载)执行,可能导致意外结果。
-
-
默认导出与导入
-
解释:不同于具名导出,ES6允许使用
export default
为模块设置默认导出接口。当其他模块加载此模块时,可以为此默认导出接口指定任何名称。 -
语法
-
默认导出
1
2
3
4
5
6export default function() { /* ... */ }
// OR
function foo() { /* ... */ }
export default foo; -
默认导入
1
import customName from './xxx.js'
-
-
使用说明
-
每个模块只能有一个默认输出,这意味着在一个模块中,
export default
命令只能使用一次。 -
export default xxx
本质上是将模块的特定接口赋值给一个名为default
的变量并具名导出。当其他模块导入这个default
变量时,ES6 允许使用任何名称来引用该变量。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// example1
function add(x, y) {
return x * y;
}
export { add as default }; // 等价于 export default add;
// example2
import { default as foo } from "./xxx.js" // 等价于 import foo from "./xxx.js"
// example3
var a = 1;
export default a; // 等价于 var default = a; export { default };
// example4
export default 124; // 等价于 var default = 124; export {default};
// example5
export default var a = 1; // ❌,因为默认导出的本质是赋值,显然 var default = var a 的赋值方式是错误的,所以这里报错 -
默认导入和具名导入可以同时使用,表示同时导入默认接口和具名接口。
1
import customName, { foo } from './xxx.js'
-
整体导入
-
解释:整体导入是指将模块的所有公开接口加载到一个以星号
*
表示的对象上。在使用时,需要为此对象设置别名。这样,被导入模块的所有公开接口都将被加载到这个别名对象上。 -
语法
1
import * as customName from './xxx.js'
-
使用说明
- 通过模块整体导入创建的对象在运行时不可更改,因为这会破坏 ES6 的静态加载特性。
重导出
注意:这里的默认接口指的是默认导出的接口,具名接口指的是具名导出的接口
-
解释:在 ES6 中,
import
命令和export
命令可以复合使用,这表示先导入然后再导出同一个模块的接口,称之为重导出。实际上,这些接口并没有真正被导入到当前模块,而只是在当前模块进行了接口的转发。 -
语法
1
2
3
4export * from './xxx.js'
export { xxx } from './xxx.js'
export { xxx as customName } from './xxx.js'
export * as customName from './xxx.js' // ES2020 新增写法注意:
export customName from './xxx.js'
的语法是非法的 -
应用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// example1 将具名接口改名后转发
export { foo as myFoo } from './xxx.js'
// example2 将所有接口(具名接口)整体转发
export * from './xxx.js'
// example3 将默认接口转发
export { default } from './xxx.js'
// example4 将具名接口改为默认接口后转发
export { es6 as default } from './xxx.js'
// example5 将默认接口改为具名接口后转发
export { default as es6 } from './xxx.js'
// example5 将所有接口(默认接口 + 具名接口)封装为命名空间后转发
export * as customName from './xxx.js'
跨模块常量
-
解释:跨模块常量是指被导出并在多个模块间共享的常量。
-
实践:为了管理跨模块常量,我们通常在项目中创建一个
constants
目录,并将不同用途的常量保存在该目录下的不同文件中。在constants
目录中,我们可以创建一个index.js
文件,用于将其他文件导出的常量合并并重新导出(重导出)。这样,当需要使用跨模块常量时,只需要加载constants/index.js
文件即可。1
2
3
4
5
6// constants/db.js
export const db = {
url: 'http://my.couchdbserver.local:5984',
admin_username: 'admin',
admin_password: 'admin password'
};1
2// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];1
2
3// constants/index.js
export { db } from './db';
export { users } from './users';1
2// script.js
import {db, users} from './constants/index';
运行时加载
-
解释:ES6 模块化通过
import
命令实现静态加载模块,也就是在编译时加载模块。import
命令会在模块中最先执行,如果它位于代码块(如条件语句)中,将会引发语法错误。然而,为了实现动态加载模块,也就是在运行时加载模块,ES2020 引入了import()
函数。这个函数接受一个参数specifier
,表示模块的位置,然后在运行时加载指定的模块,并返回一个 Promise 对象。import()
函数的功能类似于 Node.js 的require
方法。 -
语法:
import(specifier)
specifier
指定了模块位置,与import
命令相同,可以是相对路径、绝对路径或模块名。 -
使用说明
import()
函数的使用时机有:按需加载模块、条件加载模块、模块的路径需要动态生成。import()
函数可以用在任何地方,包括非模块脚本(如type
属性不为module
的script
标签中的脚本)。- 当
import()
函数成功动态加载模块后,该模块会被作为一个对象传递给then
方法作为参数。模块中导出的**所有接口(包括默认接口)**都会作为这个对象的属性存在。 - 如果需要同时动态加载多个模块,你可以使用
Promise.all()
方法。在这种情况下,then
方法的参数将是一个由这些模块对象构成的数组。
模块元信息
-
解释:ES2020 引入了
import.meta
,允许在当前模块中访问模块的元信息。import.meta
是一个只能在模块内部使用的对象,它的属性取决于运行环境,没有统一的规定。 -
常用属性
-
import.meta.url
:当前模块的完整 URL。该属性可以用于构建与当前模块相关的资源路径。 -
import.meta.scriptElement
:加载当前模块的script
元素,相当于document.currentScript
(浏览器特有)。 -
import.meta.filename
:当前模块的绝对路径,相当于 CommonJS 的__filename
(Deno 特有)。 -
import.meta.dirname
:当前模块所在目录的绝对路径,相当于 CommonJS 的__dirname
(Deno 特有)。补充:Deno 是一个 JavaScript 和 TypeScript 运行时,默认使用 ES 模块化,且内置 TypeScript 支持。
-
ES6 - 模块加载
浏览器加载
-
加载非模块脚本
1
2
3
4
5
6
7<!-- 内嵌脚本 -->
<script type="application/javascript">
// 脚本内容
</script>
<!-- 外部脚本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>-
浏览器脚本的默认语言是 JavaScript,因此
type="application/javascript"
可以省略。 -
默认情况下,浏览器同步加载 JavaScript 脚本,即当渲染引擎遇到
<script>
标签时就会停下来,等到执行完脚本,再继续向下渲染。对于外部脚本,还需要包含脚本下载的时间。 -
如果
<script>
标签使用defer
或async
属性,浏览器会异步加载 JavaScript 脚本。比较 defer
async
执行时机 渲染完再执行
(脚本会在 DOM 结构完全生成后再执行)下载完就执行
(脚本会在下载完后立即执行,此时会使渲染引擎中断渲染)顺序执行 是
(多个defer
脚本会根据其在页面出现的顺序执行)否
(多个async
脚本不能保证执行顺序)
-
-
加载 ES6 模块脚本(使用
type="module"
属性)1
2
3
4
5
6
7<!-- 内嵌脚本 -->
<script type="module">
// 脚本内容
</script>
<!-- 外部脚本 -->
<script type="module" src="path/to/myModule.js">
</script>- 浏览器异步加载 ES6 模块,相当于自带了
defer
属性。此时,模块脚本会等待整个页面渲染完再执行,且多个模块脚本会按照其在页面出现的顺序执行。但是如果此时使用了async
属性,模块脚本只要下载完成就会立即执行,且多个模块脚本之间的执行顺序也无法保证。 - 模块脚本的特点
- 模块作用域:代码运行的作用域是模块作用域,而不是全局作用域。因此模块内部的顶层变量,外部不可见。
- 严格模式:自动使用严格模式(相当于声明了 “use strict”),此时模块顶层
this
的值是undefined
。 - 导入导出:可以使用
import
命令加载其他模块(不能省略.js
后缀),使用export
命令向外暴露接口。 - 执行一次:同一个模块加载多次时,只执行一次。
- 浏览器异步加载 ES6 模块,相当于自带了
Node.js 加载
加载区别
Node.js 支持 CommonJS 模块(CJS)和 ES6 模块(MJS),二者不兼容,区别有
-
CommonJS 模块使用
require
和module.export
;ES6 模块使用import
和export
。 -
CommonJS 模块采用
.cjs
后缀;ES6 模块采用.mjs
后缀。package.json 文件中,如果指定
"type": "module"
,那么.js
文件以 ES6 模块加载;如果指定"type": "commonjs"
或不指定,则.js
文件以 CommonJS 模块加载。ES6 模块与 CommonJS 模块不要混用!
Node.js 加载 ES6 模块的注意事项。
-
不能省略脚本的后缀名,包括
import
命令和 package.json 中main
字段指定的脚本路径。1
2import { something } from './index.js'; // ✅
import { something } from './index'; // ❌ -
.mjs
文件支持 URL 路径,即路径中可以包含查询参数。同一个脚本只要参数不同,就会被加载多次,并且保存成不同的缓存。因此,只要文件名中含有:
、%
、#
、?
等特殊字符,最好对这些字符进行转义1
import './foo.mjs?query=1';
-
Node.js 的
import
命令仅支持加载本地模块(使用file:
协议)和data:
协议。远程模块加载(例如通过http:
或https:
)是不支持的。 -
在使用
import
时,只支持相对路径,也就是说路径必须以./
或../
开头。绝对路径(以/
或//
开头)是不支持的。
Node.js 中不能在 ES6 模块中使用,而可以在 CommonJS 模块中使用的内部顶层变量有,
this
(在 ES6 模块中,顶层this
指向undefined
;在 CommonJS 中,顶层this
指向当前模块)arguments
require
module
exports
__filename
__dirname
入口配置
package.json
有两个字段可以指定模块的入口文件:main
和 exports
,其中 exports
字段的用法更加复杂,优先级高于 main
字段。
-
main
指定模块加载的入口。1
2
3
4
5// ./node_modules/es-module-package/package.json
{
"type": "module",
"main": "./src/index.js"
}1
2
3
4// ./my-app.mjs
import { something } from 'es-module-package';
// 加载 ./node_modules/es-module-package/src/index.js -
exports
指定脚本或子目录别名。1
2
3
4
5
6
7// ./node_modules/es-module-package/package.json
{
"exports": {
"./submodule": "./src/submodule.js", // 脚本别名
"./features/": "./src/features/" // 子目录别名
}
}1
2
3
4
5
6
7// ./my-app.mjs
import submodule from 'es-module-package/submodule';
// 加载 ./node_modules/es-module-package/src/submodule.js
import feature from 'es-module-package/features/x.js';
// 加载 ./node_modules/es-module-package/src/features/x.js注意:如果没有通过
exports
关键字指定别名,此时就不能使用 “模块名+脚本名” 来加载模块中的指定脚本。1
2
3
4
5
6
7// ./my-app.mjs
// 报错 ❌
import submodule from 'es-module-package/private-module.js';
// 不报错 ✅
import submodule from './node_modules/es-module-package/private-module.js'; -
exports
指定模块加载的主入口,优先级高于main
字段。1
2
3
4
5
6
7
8
9
10{
"exports": {
".": "./main.js" // 完整写法,别名 "." 表示模块的主入口
}
}
// 等同于
{
"exports": "./main.js" // 简写形式,"exports" 直接表示模块的主入口
}注意:
exports
字段只有支持 ES6 的 Node.js 才认识,因此可以搭配main
字段,来兼容旧版本的 Node.js。1
2
3
4
5
6{
"main": "./main-legacy.cjs", // 老版本 Node.js 的入口文件
"exports": {
".": "./main-modern.cjs" // 新版本 Node.js 的入口文件
}
} -
exports
指定 CommonJS 和 ES6 的模块加载的主入口。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17{
"type": "module",
"exports": {
".": {
"require": "./main.cjs", // 使用 require() 命令的入口文件 - CommonJS 入口
"default": "./main.js" // 使用 import 命令的入口文件 - ES6 入口
}
}
}
// 等同于
{
"exports": {
"require": "./main.cjs", // 简写形式
"default": "./main.js" // 简写形式
}
}注意:如果通过
exports
关键字还设置了其他别名,此时就不能使用简写形式来进行条件加载。1
2
3
4
5
6
7
8{
// 报错 ❌
"exports": {
"./feature": "./lib/feature.js",
"require": "./main.cjs",
"default": "./main.js"
}
}
兼容加载
-
CommonJS 模块中加载 ES6 模块:使用
import()
方法。1
2
3(async () => {
await import('./my-app.mjs');
})(); -
ES6 模块中加载 CommonJS 模块:使用
import
命令整体加载。1
2
3
4
5
6
7
8
9// 正确 ✅
import packageMain from 'commonjs-package';
// 报错 ❌
import { method } from 'commonjs-package';
// 正确 ✅(加载单一的输出项)
import packageMain from 'commonjs-package';
const { method } = packageMain;另一种不建议的在 ES6 模块中加载 CommonJS 模块的方法是,使用 Node.js 内置的
module.createRequire()
方法。这种写法将 ES6 和 CommonJS 用法混合,因此不建议使用。1
2
3import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const cjs = require('./xxx.cjs'); -
Node.js 的内置模块使用
import
命令加载时,既可以整体加载,也可以加载指定的输出项。1
2
3
4
5// 整体加载
import EventEmitter from 'events';
// 加载指定的输出项
import { readFile } from 'fs';
兼容支持
-
ES6 模块允许被 CommonJS 模块加载:在 ES6 模块中添加一个整体输出接口,如
export default obj
。 -
CommonJS 模块允许被 ES6 模块加载:给 CommonJS 模块添加一个包装层(是一个符合 ES6 模块化规范的文件),该文件中先整体输入 CommonJS 模块,再根据需要输出具名接口。
1
2import cjsModule from '../index.js';
export const foo = cjsModule.foo;为了让包装层符合 ES6 模块化规范,可以把这个文件的后缀名改为
.mjs
,或者将它放在一个子目录,再在这个子目录里面放一个单独的package.json
文件,指明{ type: "module" }
。 -
如果希望一个模块同时被
require()
或import
加载,那么可以使用 package.json 中的exports
字段设置条件加载,即指明不同格式的模块各自加载的入口。
ES6 模块化 Vs. CommonJS 模块化
区别 | ES6 | CommonJS |
---|---|---|
输出类型 | 值的引用 | 值的拷贝 |
加载时机 | 编译时加载 | 运行时加载 |
加载方式 | import 异步加载 |
require() 同步加载 |
进一步解释
-
CommonJS 加载的模块是通过
module.exports
暴露出的对象,该对象在脚本运行完后生成。ES6 加载的模块是一种静态定义,在代码静态解析阶段生成。 -
CommonJS 模块输出的是值的拷贝,ES6 模块输出的是值的引用,通过以下代码可以清楚感知到其区别!
1
2
3
4
5
6
7
8
9// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};1
2
3
4
5
6// main.js
var mod = require('./lib'); // or import mod from './lib'
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3 for CommonJS; 4 for ES6-
CommonJS 输出分析:
mod.incCounter()
方法的调用并未影响mod.counter
的值,这是因为加载模块时,会对模块所有暴露的接口进行缓存。如果想通过lib.js
模块的incCounter()
方法来修改暴露的counter
,需要将counter
属性设置为一个getter
。1
2
3
4
5
6
7// lib.js
module.exports = {
get counter() {
return counter;
}
incCounter: incCounter,
}; -
ES6 输出分析:
mod.incCounter()
方法的调用影响了mod.counter
的值,这是因为 JS 在进行静态分析时,遇到import
命令后,会生成一个只读引用。当脚本执行时,会通过这个只读引用去加载的模块取值。ES6 模块不会缓存运行结果,而是动态地从被加载的模块中取值,因此引入的模块接口都是只读的。
-
-
其他关于 CommonJS-值的拷贝 与 ES6-值的引用 区别的示例代码如下,
1
2
3
4
5
6
7
8
9
10
11
12// mod.js
function C() {
this.sum = 0;
this.add = function () {
this.sum += 1;
};
this.show = function () {
console.log(this.sum);
};
}
export let c = new C();1
2
3
4
5
6
7
8
9
10
11// x.js
import {c} from './mod';
c.add();
// y.js
import {c} from './mod';
c.show();
// main.js
import './x';
import './y'; // 0 for CommonJS; 1 for ES6
循环加载
循环加载(circular dependency)指的是,a 脚本的执行依赖 b 脚本,而 b 脚本的执行又依赖 a 脚本。
-
CommonJS 模块化中的循环加载
-
模块加载原理:在 CommonJS 中,当你使用
require
命令第一次加载特定模块时,该模块会被执行,并在内存中生成一个对象。之后,当你需要使用这个模块时,就会从这个内存对象的exports
属性中取值。即使你再次执行require
命令,该模块也不会再次执行,而是直接从缓存中取值。因此,无论你加载一个 CommonJS 模块多少次,它都只会在第一次加载时运行一次,之后的加载都会返回第一次运行的结果,除非你手动清除系统缓存。1
2
3
4
5
6{
id: '...', // 模块名称
exports: { ... }, // 模块输出的各个接口
loaded: true, // 表示该模块的脚本是否执行完毕
...
} -
循环加载策略:一旦出现某个模块被 “循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。
1
2
3
4
5
6
7
8
9
10
11// a.js
exports.done = false; // <1>
var b = require('./b.js'); // <2>
/*
模块 a 暂时挂起,等待模块 b 加载完成。
此时 a 的 exports 属性已经初始化,但可能还未达到最终状态。
*/
console.log('在 a.js 之中,b.done = %j', b.done); // <8>
exports.done = true; // <9>
console.log('a.js 执行完毕'); // <10>
/* 模块 a 的执行完成 */1
2
3
4
5
6
7
8
9
10
11
12
13// b.js
exports.done = false; // <3>
var a = require('./a.js'); // <4>
/*
此时模块 a 被循环加载,只能访问 a 的当前导出状态,即 exports: { done: false }。
在循环加载时,不会暂停当前模块的执行,而是使用模块 a 已经导出的值。
*/
console.log('在 b.js 之中,a.done = %j', a.done); // <5>
exports.done = true; // <6>
console.log('b.js 执行完毕'); // <7>
/*
模块 b 执行完毕后,控制权返回给模块 a,模块 a 继续执行。
*/1
2
3
4
5
6
7
8
9
10
11// main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
/* Output:
在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true
*/ -
安全的模块加载方式
1
2var a = require('a'); // 安全的写法 ✅
var foo = require('a').foo; // 危险的写法(如果发生循环加载,值后续可能会被改写) ❌
-
-
ES6 模块化中的循环加载
-
循环加载策略:一旦某个模块被 “循环加载”,就会认为使用的该模块的相关接口已经存在,继续向下执行。
1
2
3
4
5// a.mjs
import {bar} from './b'; // <1>
console.log('a.mjs'); // <6>
console.log(bar()); // <7>
export let foo = 'foo'; // <8>1
2
3
4
5// b.mjs
import {foo} from './a'; // <2>
console.log('b.mjs'); // <3>
console.log(foo()); // <4> 未定义报错
export let bar = 'bar'; // <5>
-
TS - 模块
简要概述
任何包含 import
或 export
语句的文件,就是一个模块(module)。
-
如果文件不包含
export
语句,就是一个全局的脚本文件。 -
模块本身就是一个作用域,不属于全局作用域。
-
暴露给外部的接口,必须用
export
命令声明;如果其他文件要使用模块的接口,必须用import
命令来输入。 -
如果一个文件不包含
export
语句,但是希望把它当作一个模块(即内部变量对外不可见),可以在脚本头部添加一行语句如下,此时当前文件被当作模块处理。1
export {};
-
TypeScript 允许加载模块时,省略模块文件的后缀名。
模块语法
注意-1:这里的类型包括
type
或interface
关键字声明的类型。
注意-2:TypeScript 中通过
export
导出的内容分为正常接口和类型。
TypeScript 支持所有 ES6 模块语法,此外还支持类型的导出和导入。
注意:以下示例中,假设 A 是类型,a 是正常接口(变量、类、函数)。
-
类型导出
-
语法一(不推荐,难以区分类型和正常接口)
1
2
3
4
5
6
7export interface A { foo: string; }
export let a = 1;
// 等同于(简写形式)
interface A { foo: string; }
let a = 1;
export { A, a }; -
语法二
1
export { type A, a };
-
语法三
1
2export type { A };
export { a };
-
-
类型导入
-
语法一(不推荐,难以区分类型和正常接口)
1
import {A, a} from './my_module'
-
语法二
1
import { type A, a } from './my_module'
-
语法三
1
2import type { A } from './my_module';
import { a } from './my_module';
-
-
使用说明
-
TypeScript 还支持默认导出导入和整体导入
1
2
3
4
5
6
7
8
9
10
11// 默认导出
export default interface User { name: string }; // 默认导出 - interface 定义的类型
type Stu = { grade: number }; // 默认导出 - type 定义的类型
export default Stu;
// 默认导入 - 引入模块的默认导出类型
import type DefaultType from './my_module'
// 整体导入 - 将模块的所有导出类型作为一个命名空间引入
import type * as TypeNS from './my_module' -
对于一个类,如果使用
export { className }
导出,那么导出的是一个正常接口;如果使用export type { className }
导出,那么导出的是一个类型。
-
CommonJS 模块处理
-
模块导入
-
import=
语句1
import fs = require('fs');
-
import * as [模块名] from "模块地址"
1
import * as fs from 'fs';
-
-
模块导出:
export=
语句1
2
3let obj = { foo: 123 };
export = obj;注意:
export=
语句输出的对象,只能使用import =
语句加载。
类型编译
TypeScript 提供编译选项 importsNotUsedAsValues
来指定导入类型的 import
命令的编译方式。该编译选项的可取值为,
remove
:默认值。删除导入类型的import
语句。preserve
:保留类型的import
语句,将其变为 “import
模块位置” 的形式,从而保留模块中的副作用。error
:保留类型的import
语句,与preserve
相同,但是导入类型必须使用import type
的形式,否则报错。
假设 .ts
脚本中有以下代码,
1 | import { TypeA } from './a'; |
-
"importsNotUsedAsValues": "remove"
时,编译结果为空。 -
"importsNotUsedAsValues": "preserve"
时,编译结果为1
import './a';
-
"importsNotUsedAsValues": "error"
时,编译报错,除非原始的.ts
脚本中的代码如下,此时编译结果与"preserve"
的编译结果相同。1
import type { TypeA } from './a';
模块定位
模块定位(module resolution)指的是,一种用来确定 import
语句和 export
语句里面的模块文件位置的算法。
相对、非相对模块
-
相对模块(relative import):指的是路径以
/
、./
、../
开头的模块。相对模块根据当前脚本所在位置进行计算,通常用于保存在当前项目目录结构中的模块脚本。1
2
3import Entry from "./components/Entry"; // 相对路径
import { DefaultHeaders } from "../constants/http"; // 相对路径
import "/mod"; // 绝对路径 -
非相对模块(non-relative import):指的是不带有路径信息的模块。非相对模块根据
baseUrl
属性或模块映射来确定,通常用于加载外部模块。1
2import * as $ from "jquery";
import { Component } from "@angular/core";
Classic、Node 方法
TypeScript 使用编译参数 moduleResolution
来确定模块定位算法,常用的算法有两种:Classic
和 Node
。其中 Node
方法就是模拟 Node.js 的加载方法,即 require()
的实现方法。
根据编译参数 module
值的不同,编译参数 moduleResolution
有不同的默认值,"module": "commonjs"
时,默认值为 "node"
,否则为 "classic"
。
模块定位方法 | Classic 方法 | Node 方法 |
---|---|---|
相对模块 | 以当前脚本的路径作为基准路径,计算相对模块的位置。 | 以当前脚本的路径作为基准路径, 1. 首先检查当前目录中是否存在与模块名称对应的 .ts 、.tsx 、.d.ts 文件。如果找到,直接加载该文件。2. 如果没有找到对应的文件,接下来检查当前目录中是否存在与模块名称对应的目录。 如果该目录存在,进一步检查目录下的 package.json 文件。 如果 package.json 文件存在,并且 types 字段指定了模块的入口文件,则加载该入口文件。3. 如果 package.json 文件不存在或 types 字段未指定入口文件,查找该目录下的 index.ts 、index.tsx 、或 index.d.ts 文件。 如果找到其中一个文件,则加载该文件。4. 如果以上步骤均未成功找到模块,则抛出错误,提示无法找到指定模块。 |
非相对模块 | 以当前脚本的路径作为起点,一层层查找上级目录。 | 1. 首先检查当前目录的 node_modules 子目录中是否存在文件 .ts 、.tsx 或 .d.ts 。如果找到,直接加载该文件。2. 如果没有找到对应的文件,检查 node_modules 子目录中是否存在 package.json 文件。 如果 package.json 文件存在,并且 types 字段指定了入口文件,则加载该入口文件。3. 如果 package.json 文件没有指定入口文件,检查 node_modules 中是否存在 @types 子目录。 在 @types 目录中查找 .d.ts 文件,如果找到,则加载该文件。4. 如果没有找到 @types 目录或文件,检查 node_modules 中是否存在与模块名称相同的子目录。 在子目录中查找 index.ts 、index.tsx 或 index.d.ts 文件。如果找到其中一个文件,则加载该文件。5. 如果在当前目录中未找到模块,进入上一层目录,并重复上述步骤。 继续向上查找,直到找到模块或到达文件系统的根目录为止。 |
路径映射配置
TypeScript 提供了以下 tsconfig.json
中的配置,来指定脚本模块的路径。
-
compilerOptions.baseUrl
:指定脚本模块的基准目录。1
2
3
4
5{
"compilerOptions": {
"baseUrl": "." // "." 表示基准目录是 tsconfig.json 所在的目录。
}
} -
compilerOptions.paths
:指定非相对模块与实际脚本的映射,常与baseUrl
配合使用。1
2
3
4
5
6
7
8{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"jquery": ["node_modules/jquery/dist/jquery"] // 加载 jquery 时,实际加载的脚本为 node_modules/jquery/dist/jquery(结合 baseUrl 进行计算)
}
}
}注意:每个非相对模块名的值是一个数组,可以指定多个路径。如果第一个脚本路径不存在,那么就加载第二个路径,以此类推。
-
compilerOptions.rootDirs
:指定模块定位时需要查找的其他目录。该配置指定一个目录列表,此时这些目录在编译时被视为一个虚拟的根目录,也就是说 TypeScript 编译器会将这些目录中的文件当作在同一个目录中一样处理。1
2
3
4
5
6{
"compilerOptions": {
"rootDirs": ["src/zh", "src/de", "src/#{locale}"]
}
}
模块编译参数
-
--traceResolution
参数,表示编译时在命令行显示模块定位的每一步。1
$ tsc --traceResolution
-
--noResolve
参数,表示模块定位时,只考虑在命令行传入的模块。1
2import * as A from "moduleA";
import * as B from "moduleB";1
$ tsc app.ts moduleA.ts --noResolve # 报错,因为 moduleB 模块无法被定位
TS - 命名空间
基本使用
-
命名空间:
namespace
是 TypeScript 在 ES6 之前用来组织相关代码(模块)的方式。TypeScript 允许使用namespace
关键字创建一个容器,内部的所有变量和函数,都必须在这个容器里边使用。1
2
3
4
5
6
7
8
9namespace AssertUtils {
function isString(value: unknown): value is string {
return typeof value === 'string';
}
console.log(isString(123)); // ✅
}
// console.log(AssertUtils.isNumber('123')); // ❌ -
语法
namespace customName { ... }
-
使用说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58// 1. 给命名空间的内部成员加上 export 前缀,此时就可以在命名空间外部访问命名空间的内部成员。
namespace Constants {
export const PI = Math.PI;
export const E = Math.E;
}
console.log(Constants.PI, Constants.E);
// 2. 命名空间的编译结果为一个 JavaScript 对象,此时凡命名空间中包含 export 前缀的内部成员,都成为了该对象的属性。
// 比如,Constants 的编译结果为 { "PI": 3.141592653589793, "E": 2.718281828459045 }
// 3. 可以在命名空间中使用 import 命令导入其他命名空间的成员,同时为该成员设置别名。也可以在命名空间外部使用 import 命令,同时设置别名。
namespace MathConstants {
import MathPI = Constants.PI;
import MathE = Constants.E;
console.log("in MathConstants", MathPI, MathE);
}
import PI = Constants.PI;
console.log('import from Constants', PI);
// 4. 命名空间可以嵌套,但是如果要在外部使用被嵌套的命名空间,则要给其添加 export 前缀。
namespace Jack {
export namespace DetailInfo {
export let age = 18;
}
}
console.log('jack\'s age is', Jack.DetailInfo.age)
// 5. 命名空间中不仅可以包含实义代码,还可以包括类型代码。
namespace N {
export interface MyInterface { };
export type MyType = {};
export class MyClass { };
}
// 6. 命名空间与模块的作用一致,都用于将相关代码组织在一起,并对外输出接口。一个文件只能有一个模块(更建议使用),但可以有多个命名空间。
// 7. 可以使用三斜杠指令来引入存放在单个文件中的命名空间代码。
/// <reference path = "SomeFileName.ts">
// 8. 命名空间可以使用 export 命令导出,在其他文件中使用 import 命令导入。
export namespace Shapes {
export class Triangle {
// ...
}
}
/*
其他脚本中使用 import 语法如下导入命名空间 Shapes 的 Triangle 类
- 方法一
import { Shapes } from "./xxx.ts"
const t = new Shapes.Triangle();
- 方法二
import * as shapes from './xxx.ts
const t = new shapes.Shapes.Triangle();
*/
命名空间合并
-
命名空间合并:与
interface
类似,多个同名的namespace
会自动合并,这有利于对代码的扩展。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15namespace Animals {
export class Cat { }
}
namespace Animals {
export class Dog { }
}
// 等同于
/*
namespace Animals {
export class Cat{}
export class Dog{}
}
*/ -
使用说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 1. 命名空间合并时,没有使用 export 前缀的成员不会被合并,其只能在各自的命名空间中使用。
// 2. 命名空间可以和同名函数、类、Enum 合并,但是函数、类、Enum 必须在命名空间之前声明,此时同名的命名空间相当于给这个函数、类、Enum 对象添加额外的属性。
function f() {
console.log((f as any).version);
}
namespace f {
export const version = '1.0'
}
f(); // '1.0'
// 3. 命名空间合并时,命名空间中使用 export 前缀的成员不能与 Enum 成员同名。
enum Direction {
Left,
Right
}
namespace Direction {
export const Left = 1; // ❌
}
TS - 装饰器(标准语法)
简要介绍
-
装饰器(Decorator):一种特殊的函数,可以附加到类、方法、访问器、属性上,用于修改或扩展其行为。
-
装饰器是函数
-
装饰器通过
"@ + 函数"
或"@ + 工厂函数(参数)"
的方式使用(工厂函数就是会返回一个函数的函数) -
装饰器接受所修饰对象的一些相关值作为参数
-
装饰器要么不返回值,要么返回一个新对象来取代所修饰的对象
-
装饰器只能应用于类及其内部成员,不能用于独立的函数
-
-
装饰器版本:TypeScript 支持两种装饰器语法:
-
传统语法:TypeScript 最初支持的语法。适用于 TypeScript 4.x 或更早版本,需要配置如下,
1
2
3
4
5
6{
"compilerOptions": {
"experimentalDecorators": true, // must
"emitDecoratorMetadata": true // could 该编译选项用于控制是否产生一些装饰器的元数据,供某些模块(如 reflect-metadata)使用
}
} -
标准语法:符合 ECMAScript 标准的语法。适用于 TypeScript 5.0 或更高版本,需要配置如下,
1
2
3
4
5
6{
"compilerOptions": {
"experimentalDecorators": false, // must
"useDefineForClassFields": true // could 该编译选项用于控制是否使用 Object.defineProperty 来初始化类字段
}
}
-
-
装饰器的类型定义(函数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14type Decorator = (
target: DecoratedValue, // 所装饰的对象
context: { // 上下文对象,包含有关所装饰对象的信息。可以使用 TypeScript 提供的原生接口 DecoratorContext 来描述
kind: string; // 所装饰对象的类型,可取值为 'class', 'method', 'getter', 'setter', 'field', 'accessor',即对应六种类型的装饰器
name: string | symbol; // 所装饰对象的名字,如属性名、类名等
addInitializer?(initializer: () => void): void; // 添加类的初始化逻辑的函数
static?: boolean; // 所装饰对象是否为类的私有成员
private?: boolean; // 所装饰对象是否为类的静态成员
access: { // 某个值的 getter 和 setter 方法
get?(): unknown;
set?(value: unknown): void;
};
}
) => void | ReplacementValue;
装饰器语法
类装饰器
-
类装饰器的类型定义
1
2
3
4
5
6
7
8type ClassDecorator<T extends Function> = (
target: T, // 被装饰的类的构造函数
context: {
kind: 'class'; // 标识装饰器应用于类
name: string | undefined; // 类名,如果是匿名类则为 undefined
addInitializer(initializer: () => void): void; // 添加类初始化逻辑的方法,通常在类的构造函数执行完毕后立即执行
}
) => T | void; // 可以返回一个新的类构造函数或不返回任何值注意:类装饰器其实就是构造方法装饰器
-
使用示例
-
定义一个类装饰器,用于在所装饰的类的原型上添加一个
greet
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 声明构造函数类型
interface UserConstructor { new(...args: any[]): {} }
// 扩展类的接口(避免 TypeScript 语法错误)
interface User {
greet(): void
}
// 定义类装饰器
function Greeter(target: UserConstructor, context: ClassDecoratorContext) {
if (context.kind === 'class') {
target.prototype.greet = function () {
console.log("Hello")
}
}
}
// 使用类装饰器
class User { }
const u = new User();
u.greet(); // 'Hello' -
定义一个类装饰器,返回一个函数,用于替代所装饰的类的构造方法,并在其中实现了实例的计数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// 声明构造函数类型
interface MyClassConstructor { new(...args: any[]): MyClass }
// 扩展类的接口
interface MyClass {
count: number
}
// 定义类装饰器
function countInstances(target: MyClassConstructor, context: ClassDecoratorContext) {
let instanceCount = 0; // 实例统计变量,每创建一个实例对象,计数 + 1
const wrapper = function (...args: any[]) { // 要返回的构造函数
instanceCount++;
const instance = new target(...args); // 构造函数创建的实例对象
instance.count = instanceCount; // 实例属性 count,表示当前实例的编号
return instance;
} as unknown as typeof MyClass;
wrapper.prototype = target.prototype; // 为了确保 wrapper 这个构造函数通过 instance of MyClass 运算符的检查
return wrapper;
}
// 使用类装饰器
class MyClass { }
const instance1 = new MyClass();
const instance2 = new MyClass();
console.log(instance1.count, instance2.count); // 1, 2
console.log(instance1 instanceof MyClass); // true -
定义一个类装饰器,返回一个新类,用于替代所装饰的类,并在其中实现了实例的计数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// 声明构造函数类型
interface NewClassConstructor { new(...args: any[]): NewClass }
// 扩展类的接口
interface NewClass { count: number }
// 定义类装饰器
function CountNewInstances(target: NewClassConstructor, context: ClassDecoratorContext) {
let instanceCount = 0;
return class extends target {
constructor(...args: any[]) {
super(...args);
instanceCount++;
this.count = instanceCount;
}
}
}
// 使用类装饰器
class NewClass { }
const instance3 = new NewClass();
const instance4 = new NewClass();
console.log(instance3.count, instance4.count); // 1, 2
console.log(instance3 instanceof NewClass); // true -
定义一个类装饰器,返回一个函数,用于替代所装饰类的构造方法,但是禁止该构造方法通过
new
命令新建类的实例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 补充:new.target 是 ES6 引入的元属性,用于在构造函数/方法中检测其是否通过 new 关键字调用,如果是 new.target 指向被调用的构造函数/被实例化的类,否则是 undefined
// 声明构造函数类型
interface PersonConstructor { new(...args: any[]): Person }
// 定义类装饰器
function fucntionCallable(target: PersonConstructor, context: ClassDecoratorContext) {
return function (...args: any[]) {
if (!new.target)
return new target(...args);
throw new Error('该函数不可以使用 new 命令调用')
} as unknown as typeof Person;
}
// 使用类装饰器
class Person {
constructor(public name: string) { }
}
// @ts-ignore 该注释告诉 TypeScript 编译器忽略下一行代码中的类型检查错误
const jack = Person('jack');
console.log(jack.name); // 'jack' -
定义一个装饰器工厂,其返回的装饰器将所装饰的类注册为自定义的 HTML 元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// 补充-1:customElements.define(name, constructor) 用于将一个 JavaScript 类(通常是继承自 HTMLElement 的类)与一个自定义的 HTML 标签关联起来,从而创建新的 HTML 元素,并定义这些元素的行为和属性。
// 参数解释
// - name 字符串,表示自定义元素的名称(必须包含连字符,以区分内置 HTML 元素)
// - constructor 一个 JavaScript 类,通常是继承自 HTMLElement 的类,定义了自定义元素的行为和属性
// 注意事项:constrctor 通常继承自 HTMLElement 的类,同时可以在其中定义生命周期回调(如 connectedCallback,用于在元素被插入到文档中时执行特定操作)
// 补充-2:类装饰器的 context.addInitializer(initializer) 用于注册一段初始化逻辑。接受一个函数作为参数,同时该函数在类的构造函数执行完成后立即执行。
// 注意事项:context.addInitializer(initializer) 可以多次调用,来注册多个初始化逻辑,这些逻辑会按照注册顺序依次执行
// 声明构造函数类型
interface MyComponentConstructor { new(...args: any[]): MyComponent };
// 定义装饰器工厂
function customElement(name: string) {
return function (target: MyComponentConstructor, context: ClassDecoratorContext) {
context.addInitializer(function(){
customElements.define(name, target);
})
}
}
// 使用装饰器工厂
'hello-world') (
class MyComponent extends HTMLElement {
constructor() {
super();
}
connectedCallback() { // 当 <hello-world> 标签被插入到 DOM 时调用
this.innerHTML = `<h1>Hello World</h1>`
}
}
-
方法装饰器
-
方法装饰器的类型定义
1
2
3
4
5
6
7
8
9
10
11type ClassMethodDecorator<T extends Function> = (
target: T, // 被装饰的类的方法
context: {
kind: 'method'; // 标识装饰器应用于方法
name: string | symbol; // 方法名
static: boolean; // 所修饰的方法是否为静态方法
private: boolean; // 所修饰的方法是否为私有方法
access: { get: () => unknown }; // 访问器对象,用于获取方法的值
addInitializer(initializer: () => void): void; // 添加方法初始化逻辑的方法,通常在类的构造函数执行期间执行,早于属性的初始化
}
) => T | void; -
使用示例
-
定义一个方法装饰器,返回一个函数,用于替代所装饰的原始方法
1
2
3
4
5
6
7function replaceMethod(target: Function, context: ClassMethodDecoratorContext) {
/* 知识回顾:TypeScript 允许函数增添一个名为 this 的参数,位于参数列表的首位,用于描述函数内部 this 的类型。
在编译时,TypeScript 会对函数进行检查,如果函数参数列表的第一个参数名为 this,那么编译的结果将会删除这个参数。 */
return function (this: Person) { // 该方法会替换方法装饰器所装饰的方法
return `How are you, ${this.name}?`;
}
} -
定义一个方法装饰器,返回一个函数,用于替代所装饰的原始方法,在不改变原始方法功能的前提下,引入日志记录功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31function log(target: Function, context: ClassMethodDecoratorContext) {
return function (this: Person, ...args: any[]) { // 该方法会替换方法装饰器所装饰的方法
console.log(`LOG: Entering method ${String(context.name)}`);
const result = target.call(this, ...args);
console.log(`LOG: Existing method ${String(context.name)}`);
return result;
}
}
class Person {
constructor(public name: string) { };
// 使用方法装饰器
hello() {
return `Hi ${this.name}`;
}
// 使用方法装饰器
greet() {
return `Hello, my name is ${this.name}`
}
}
const jack = new Person('jack');
console.log(jack.hello()); // 'How are you, jack?'
console.log(jack.greet());
// 'LOG: Entering method greet'
// 'LOG: Existing method greet'
// 'Hello, my name is jack' -
定义一个装饰器工厂,其返回的方法装饰器可以让所装饰的方法延迟执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21function delay(milliseconds: number = 0) {
return function (target: Function, context: ClassMethodDecoratorContext) {
if (context.kind === 'method') {
return function (this: Logger, ...args: any[]) { // 该方法会替换方法装饰器所装饰的方法
setTimeout(() => {
target.call(this, ...args);
}, milliseconds)
}
}
}
}
class Logger {
1000) (
log(msg: string) {
console.log(msg);
}
}
const logger = new Logger();
logger.log('Hello World'); -
定义一个方法装饰器,通过
addInitializer()
添加初始化逻辑,将方法的this
绑定到新创建的实例,从而确保方法在任何上下文中调用时,this
始终指向类的实例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 补充:context.addInitializer(initializer) 注册的初始化逻辑在构造函数执行期间执行
// 注意事项:与类装饰器类似,方法装饰器的 context.addInitializer(initializer) 也可以多次调用,用来注册多个初始化逻辑,并按照注册顺序依次执行
function Bound(target: Function, context: ClassMethodDecoratorContext) {
const methodName = context.name as string;
if (context.private)
throw new Error(`不能给私有方法 ${methodName} 绑定 this`);
context.addInitializer(function (this: any) {
this[methodName] = target.bind(this);
})
}
class Animal {
constructor(public name: string) { };
eat(food: string) {
console.log(`${this.name} is eating ${food}`);
}
}
const eat = new Animal("Lucky").eat;
eat('meat'); // 'Lucky is eating meat' -
定义一个方法装饰器,通过
addInitializer()
添加初始化逻辑,将所修饰的方法名放入一个Set
保存,并将这个Set
作为实例属性collectedMethodKeys
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23function collect(target: Function, context: ClassMethodDecoratorContext) {
context.addInitializer(function (this: any) {
if (!this.collectedMethodKeys)
this.collectedMethodKeys = new Set();
this.collectedMethodKeys.add(context.name as string);
})
}
interface Car { collectedMethodKeys: Set<string> };
class Car {
wheel() { }
charge() { }
upkeep() { }
}
const car = new Car();
console.log(car.collectedMethodKeys); // Set (3) {"wheel", "charge", "upkeep"}
-
属性装饰器
-
属性装饰器的类型定义
1
2
3
4
5
6
7
8
9
10
11type ClassFieldDecorator = (
value: undefined, // 该参数对于属性装饰器无用
context: {
kind: 'field'; // 标识装饰器应用于属性
name: string | symbol; // 属性名
static: boolean; // 所修饰的属性是否为静态属性
private: boolean; // 所修饰的属性是否为私有属性
access: { get: () => unknown, set: (value: unknown) => void }; // 访问器对象,用于获取或设置属性的值
addInitializer(initializer: () => void): void; // 添加属性初始化逻辑的方法
}
) => (initialValue: unknown) => unknown | void; // 属性装饰器可以返回一个函数(会自动执行),参数为所装饰属性的初始值,返回值为该属性的最终值。 -
使用示例
-
定义一个属性装饰器,返回一个函数,用于计算该属性的最终值
1
2
3
4
5
6
7
8
9
10
11
12
13function twice(_: undefined, context: ClassFieldDecoratorContext) {
return function (initialValue: number) {
return initialValue * 2;
}
}
class MyNumber {
pi = 3.14;
}
const instance = new MyNumber();
console.log(instance.pi); // 6.28 -
定义一个属性装饰器,将
context.access
的getter
和setter
绑定到全局变量,以便通过该变量访问和修改装饰属性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23let access;
function exposeAccess(_: undefined, context: ClassFieldDecoratorContext) {
access = context.access;
}
class Color {
name = 'green'
}
const c = new Color();
console.log(c.name); // 'green'
console.log(access!.get(c)); // 'green'
access!.set(c, 'red');
console.log(c.name); // 'red'
/*
get: function (obj) { return obj.name; }
set: function (obj, value) { obj.name = value; }
*/
-
getter、setter 装饰器
-
getter
装饰器的类型定义1
2
3
4
5
6
7
8
9
10
11type ClassGetterDecorator = (
target: Function,
context: {
kind: 'getter';
name: string | symbol;
static: boolean;
private: boolean;
access: { get: () => unknown };
addInitializer(initializer: () => void): void;
}
) => Function | void; // getter 装饰器可以返回一个函数,取代原来的 getter(取值器) -
setter
装饰器的类型定义1
2
3
4
5
6
7
8
9
10
11type ClassSetterDecorator = (
target: Function,
context: {
kind: 'setter';
name: string | symbol;
static: boolean;
private: boolean;
access: { set: (value: unknown) => void };
addInitializer(initializer: () => void): void;
}
) => Function | void; // setter 装饰器可以返回一个函数,取代原来的 setter(存值器) -
使用示例:定义一个
getter
装饰器,返回一个函数,用于取代原来的getter
,同时实现属性的懒加载,即只在首次访问时进行计算,并将结果缓存。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39// 注意:这里涉及知识点 - 属性覆盖,即 Object.defineProperty 在实例上定义了一个新的直接属性,这个属性遮盖了原有的 getter 方法。JavaScript 引擎在查找属性时,会先在对象实例上查找直接属性,然后才会查找原型链上的属性和方法。
function lazyLoad(target: Function, context: ClassGetterDecoratorContext) {
return function (this: BigNumbers, ...args: any[]) {
console.log('lazyloading...');
const result = target.call(this, ...args);
Object.defineProperty(this, context.name, {
value: result,
writable: false
})
return result;
}
}
class BigNumbers {
get value() {
console.log('computing...');
return 'big data'
}
}
const instance = new BigNumbers();
console.log(instance.hasOwnProperty('value'),'value' in instance); // false true
console.log(instance.value);
console.log(instance.hasOwnProperty('value'),'value' in instance); // true true
console.log(instance.value);
/*
false true // value 在原型链上
lazyloading...
computing...
big data
true true // value 在实例上
big data
*/
/*
补充
- obj.hasOwnProperty(prop): boolean 检查属性 prop 是否是对象 obj 的实例属性
- prop in obj: boolean 检查属性 prop 是否是对象 obj 的属性(实例属性和原型链上的属性)
*/
accessor 装饰器
-
accessor
属性修饰符:使用accessor
修饰一个属性,等同于为该属性自动生成一对取值器和存值器。-
accessor
修饰符生成的取值器和存值器作用于私有属性,这里的私有属性和公开属性可见下述#x
和x
-
accessor
修饰符可以修饰实例属性、静态属性、私有属性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Demo {
accessor x = 1;
}
// 类似于(上述代码可以看作是下述代码的语法糖)
class Demo {
#x = 1;
get x() {
return this.#x;
}
set x(val) {
this.#x = val;
}
}
-
-
accessor
装饰器的类型定义1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18type ClassAutoAccessorDecorator = (
target: {
get: () => unknown;
set: (value: unknown) => void;
},
context: {
kind: "accessor";
name: string | symbol;
access: { get(): unknown, set(value: unknown): void };
static: boolean;
private: boolean;
addInitializer(initializer: () => void): void;
}
) => {
get?: () => unknown;
set?: (value: unknown) => void;
init?: (initialValue: unknown) => unknown;
} | void; // accessor 装饰器可以返回一个对象,用于替代原有的 get() 和 set() 方法。此外,它还可以包含一个 init() 方法,该方法接受属性在类实例化时的初始值作为参数,并返回该属性的最终初始值。 -
使用示例:定义一个
accessor
装饰器,为属性的存值器和取值器添加日志输出功能1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45function log(target: ClassAccessorDecoratorTarget<Demo, number>, context: ClassAccessorDecoratorContext) {
const { kind, name } = context;
if (kind === 'accessor') {
const { get, set } = target;
return {
get() {
console.log(`getting ${name as string}`);
return get.call(this as any);
},
set(val: number) {
console.log(`setting ${name as string} to ${val}`);
return set.call(this as any, val);
},
init(initialValue: number) {
console.log(`initializing ${name as string} with ${initialValue}`);
return initialValue;
}
}
}
return;
}
class Demo {
value: number = 0; accessor
constructor(value: number) {
this.value = value;
}
}
const d = new Demo(124);
d.value = 125;
console.log(d.value);
/*
"initializing value with 0"
"setting value to 124"
"setting value to 125"
"getting value"
125
*/
export { }
装饰器的执行顺序
-
装饰器的执行流程:评估阶段 evaluation + 应用阶段 application
-
评估阶段:计算
@
符号后面的表达式的值,得到一个函数(按顺序评估) -
应用阶段:将评估装饰器后得到的函数,应用于所装饰对象(按优先级应用)
-
注意事项
- 如果属性名或方法名是计算值,则它们在对应的装饰器评估之后,再进行计算。
- 应用阶段的优先级为:静态方法装饰器 -> 原型方法装饰器 -> 静态属性装饰器 -> 实例属性装饰器 -> 类装饰器
- 实例属性值在类初始化阶段并不执行,直到类实例化时才执行;静态属性值在类初始化最后执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40function d(str: string) {
console.log(`评估 @d(): ${str}`);
return (
value: any, context: any
) => console.log(`应用 @d(): ${str}`);
}
function log(str: string) {
console.log(str);
return str;
}
'类装饰器') (
class T {
'静态属性装饰器') (
static staticField = log('静态属性值');
'原型方法') (
[log('计算方法名')]() { }
'实例属性') (
instanceField = log('实例属性值');
'静态方法装饰器') (
static fn() { }
}
/*
评估 @d(): 类装饰器
评估 @d(): 静态属性装饰器
评估 @d(): 原型方法
计算方法名
评估 @d(): 实例属性
评估 @d(): 静态方法装饰器
应用 @d(): 静态方法装饰器
应用 @d(): 原型方法
应用 @d(): 静态属性装饰器
应用 @d(): 实例属性
应用 @d(): 类装饰器
静态属性值
*/
-
-
使用说明:如果一个方法或属性有多个装饰器,则内层的装饰器先执行,外层的装饰器后执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24function logger(target: Function, context: ClassMethodDecoratorContext) {
console.log('logger 装饰器执行');
}
function bound(target: Function, context: ClassMethodDecoratorContext) {
console.log('bound 装饰器执行');
}
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
/*
logger 装饰器执行
bound 装饰器执行
*/
TS - 装饰器(传统语法)
装饰器语法
类装饰器
方法装饰器
属性装饰器
存取器装饰器
参数装饰器
装饰器的执行顺序
TS - tsconfig.json
-
解释:
tsconfig.json
是 TypeScript 项目的配置文件,位于项目的根目录。如果想用 TypeScript 处理 JavaScript 项目,此时可使用配置文件
jsconfig.json
。tsc
编译器编译 TypeScript 代码时,首先在当前目录搜索,如果不存在则在上一级目录搜索,直到找到为止。tsc
编译时,可以使用命令行参数--project
或-p
来指定配置文件tsconfig.json
的位置(目录或文件)。 -
生成
tsconfig.json
-
方式一:使用
tsc
,命令自动生成。1
$ tsc --init
-
方式二:安装 npm 的
@tsconfig
名称空间下的模块,即写好的tsconfig.json
样本,然后在tsconfig.json
中使用extends
关键字引用这个模块。1
$ npm install --save-dev @tsconfig/deno
1
2
3{
"extends": "@tsconfig/deno/tsconfig.json"
}
-
1 | { |
【纠错或】补充
- 类型统中的父子类型兼容
- 对象中的 suppressExcessPropertyErrors 编译选项的废除
- 对象中的最小兼容属性的表达不规范