目录
一、基本用法
1.TS运行环境
1.1在本地运行ts代码需要先编译,可以通过命令安装tsc,tsc 的作用就是把.ts
脚本转变成.js
脚本(先安装npm)
npm install -g typescript
1.2.通过tsc -v查询版本
2. ts编辑脚本代码
2.1编译一个或者多个ts脚本
tsc app.ts
tsc file1.ts file2.ts file3.ts
2.2同时编译多个到一个js文件--outFile
tsc file1.ts file2.ts --outFile app.js
2.3 指定保存到其他目录--outDir
tsc app.ts --outDir dist
2.4 编译指定js版本 (建议使用es2015
,或者更新版本。)--target
tsc --target es2015 app.ts
2.5 编译过程种有报错就停止编译--noEmitOnError
tsc --noEmitOnError app.ts
2.6 只检查类型是否准确不生成js文件 --noEmit
tsc --noEmit app.ts
3. tsconfig.json配置文件
TypeScript 允许将tsc
的编译参数,写在配置文件tsconfig.json
。只要当前目录有这个文件,tsc
就会自动读取,所以运行时可以不写参数。
$ tsc file1.ts file2.ts --outFile dist/app.js
上面这个命令写成tsconfig.json
,就是下面这样。
{
"files": ["file1.ts", "file2.ts"],
"compilerOptions": {
"outFile": "dist/app.js"
}
}
有了这个配置文件,编译时直接调用tsc
命令就可以了。
$ tsc
4.TS-node模块
安装ts-node
npm install -g ts-node
运行
ts-node script.ts
如果不安装 ts-node,也可以通过 npx 调用它来运行 TypeScript 脚本
$ npx ts-node script.ts
如果执行 ts-node 命令不带有任何参数,它会提供一个 TypeScript 的命令行 REPL 运行环境,你可以在这个环境中输入 TypeScript 代码,逐行执行
$ ts-node
> const twice = (x:string) => x + x;
> twice('abc')
'abcabc'
>
二、 any 类型,unknown 类型,never 类型
TypeScript 有两个“顶层类型”(any
和unknown
),但是“底层类型”只有never
唯一一个
1、any
1.1 基本含义
any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。变量类型一旦设为any
,TypeScript 实际上会关闭这个变量的类型检查。即使有明显的类型错误,只要句法正确,都不会报错。
let x:any;
x = 1; // 正确
x = 'foo'; // 正确
x = true; // 正确
TypeScript 认为,只要开发者使用了any
类型,就表示开发者想要自己来处理这些代码,所以就不对any
类型进行任何限制,怎么使用都可以。
从集合论的角度看,any
类型可以看成是所有其他类型的全集,包含了一切可能的类型。TypeScript 将这种类型称为“顶层类型”(top type),意为涵盖了所有下层。
1.1.2 类型推断问题
对于开发者没有指定类型、TypeScript 必须自己推断类型的那些变量,如果无法推断出类型,TypeScript 就会认为该变量的类型是any
。
function add(x, y) {
return x + y;
}
add(1, [1, 2, 3]) // 不报错
上面示例中,函数add()
的参数变量x
和y
,都没有足够的信息,TypeScript 无法推断出它们的类型,就会认为这两个变量和函数返回值的类型都是any
。以至于后面就不再对函数add()
进行类型检查了,怎么用都可以。这显然是很糟糕的情况,所以对于那些类型不明显的变量,一定要显式声明类型,防止被推断为any
。
当声明的时候没有赋值,没给类型也不会报错,所以在声明的同时要赋值,不然会存在安全隐患
1.1.3污染问题
ny
类型除了关闭类型检查,还有一个很大的问题,就是它会“污染”其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错
let x:any = 'hello';
let y:number;
y = x; // 不报错
y * 123 // 不报错
y.toFixed() // 不报错
上面示例中,变量x
的类型是any
,实际的值是一个字符串。变量y
的类型是number
,表示这是一个数值变量,但是它被赋值为x
,这时并不会报错。然后,变量y
继续进行各种数值运算,TypeScript 也检查不出错误,问题就这样留到运行时才会暴露。
污染其他具有正确类型的变量,把错误留到运行时,这就是不宜使用any
类型的另一个主要原因。
2.unknown
与any
含义相同,表示类型不确定,可能是任意类型,但是它的使用有一些限制,不像any
那样自由,可以视为严格版的any
unknown
类型跟any
类型的不同之处在于,它不能直接使用。主要有以下几个限制:
1.unknown
类型的变量,不能直接赋值给其他类型的变量(除了any
类型和unknown
类型)
let v:unknown = 123;
let v1:boolean = v; // 报错
let v2:number = v; // 报错
2. 不能直接调用unknown
类型变量的方法和属性。
let v1:unknown = { foo: 123 };
v1.foo // 报错
let v2:unknown = 'hello';
v2.trim() // 报错
let v3:unknown = (n = 0) => n + 1;
v3() // 报错
3. unknown
类型变量能够进行的运算是有限的,只能进行比较运算(运算符==
、===
、!=
、!==
、||
、&&
、?
)、取反运算(运算符!
)、typeof
运算符和instanceof
运算符这几种,其他运算都会报错
let a:unknown = 1;
a + 1 // 报错
a === 1 // 正确
4. 经过“类型缩小”,unknown
类型变量才可以使用。所谓“类型缩小”,就是缩小unknown
变量的类型范围,确保不会出错。
let a:unknown = 1;
if (typeof a === 'number') {
let r = a + 10; // 正确
}
unknown
可以看作是更安全的any
。一般来说,凡是需要设为any
类型的地方,通常都应该优先考虑设为unknown
类型。也视为所有其他类型(除了any
)的全集,所以它和any
一样,也属于 TypeScript 的顶层类型。
3.never
由于不存在任何属于“空类型”的值,所以该类型被称为never
,即不可能有这样的值。
never
类型的使用场景,主要是在一些类型运算之中,保证类型运算的完整性。另外,不可能返回值的函数,返回值的类型就可以写成never
如果一个变量可能有多种类型(即联合类型),通常需要使用分支处理每一种类型。这时,处理所有可能的类型之后,剩余的情况就属于never
类型。
function fn(x:string|number) {
if (typeof x === 'string') {
// ...
} else if (typeof x === 'number') {
// ...
} else {
x; // never 类型
}
}
never
类型的一个重要特点是,可以赋值给任意其他类型。
function f():never {
throw new Error('Error');
}
let v1:number = f(); // 不报错
let v2:string = f(); // 不报错
let v3:boolean = f(); // 不报错
上面示例中,函数f()
会抛错,所以返回值类型可以写成never
,即不可能返回任何值。各种其他类型的变量都可以赋值为f()
的运行结果(never
类型)
三、类型系统
1.基本类型(继承js基本数据类型)
- boolean
- string
- number
- bigint
- symbol
- object
- undefined
- null
注意:
1.上面所有类型的名称都是小写字母,首字母大写的Number
、String
、Boolean
等在 JavaScript 语言中都是内置对象,而不是类型名称
2.如果没有声明类型的变量,被赋值为undefined
或null
,它们的类型会被推断为any,
如果希望避免这种情况,则需要打开编译选项strictNullChecks
2.包装对象类型
TypeScript 对五种原始类型分别提供了大写和小写两种类型:
- Boolean 和 boolean
- String 和 string
- Number 和 number
- BigInt 和 bigint
- Symbol 和 symbol
大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量,不包含包装对象
const s1:String = 'hello'; // 正确
const s2:String = new String('hello'); // 正确
const s3:string = 'hello'; // 正确
const s4:string = new String('hello'); // 报错
String
类型可以赋值为字符串的字面量,也可以赋值为包装对象。但是,string
类型只能赋值为字面量,赋值为包装对象就会报错。(只使用小写类型,不使用大写类型)
3.Object 类型与 object 类型
3.1Oject类型
大写的Object
类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值,都是Object
类型,这囊括了几乎所有的值。(原始类型值、对象、数组、函数都是合法的Object
类型)
let obj:Object;//与let obj:{}一样,只不过后者是简写
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
除了undefined
和null
这两个值不能转为对象,其他任何值都可以赋值给Object
类型
let obj:Object;
obj = undefined; // 报错
obj = null; // 报错
3.2object类型
小写的object
类型代表 JavaScript 里面的狭义对象,即可以用字面量表示的对象(包含对象、数组和函数,不包括原始类型的值)
let obj:object;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
obj = true; // 报错
obj = 'hi'; // 报错
obj = 1; // 报错
大多数时候,我们使用对象类型,只希望包含真正的对象,不希望包含原始类型。所以,建议总是使用小写类型object
,不使用大写类型Object
注意,无论是大写的Object
类型,还是小写的object
类型,都只包含 JavaScript 内置对象原生的属性和方法,用户自定义的属性和方法都不存在于这两个类型之中
4.undefined和null的特殊性
undefined
和null
既是值,又是类型
作为值,它们有一个特殊的地方:任何其他类型的变量都可以赋值为undefined
或null
。
JavaScript 的行为是,变量如果等于undefined
就表示还没有赋值,如果等于null
就表示值为空。所以,TypeScript 就允许了任何类型的变量都可以赋值为这两个值。
let age:number = 24;
age = null; // 报错
age = undefined; // 报错
上面示例中,打开--strictNullChecks
以后,number
类型的变量age
就不能赋值为undefined
和null
。
这个选项在配置文件tsconfig.json
的写法如下。
{
"compilerOptions": {
"strictNullChecks": true
// ...
}
}
打开strictNullChecks
以后,undefined
和null
这两种值也不能互相赋值了。undefined
和null
只能赋值给自身,或者any
类型和unknown
类型的变量
let x:any = undefined;
let y:unknown = null;
5.联合类型
联合类型(union types)指的是多个类型组成的一个新类型,使用符号|
表示。
联合类型A|B
表示,任何一个类型只要属于A
或B
,就属于联合类型A|B
。
let x:string|number;
x = 123; // 正确
x = 'abc'; // 正确
6.交叉类型
交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号&
表示
交叉类型的主要用途是表示对象的合成:
let obj:
{ foo: string } &
{ bar: string };
obj = {
foo: 'hello',
bar: 'world'
};
上面示例中,变量obj
同时具有属性foo
和属性bar
。
交叉类型常常用来为对象类型添加新属性。
type A = { foo: number };
type B = A & { bar: number };
上面示例中,类型B
是一个交叉类型,用来在A
的基础上增加了属性bar
。
7.type命令
type
命令用来定义一个类型的别名。
type Age = number;
let age:Age = 55;
上面示例中,type
命令为number
类型定义了一个别名Age
。这样就能像使用number
一样,使用Age
作为类型。
8.typeof运算符
TypeScript 将typeof
运算符移植到了类型运算,它的操作数依然是一个值,但是返回的不是字符串,而是该值的 TypeScript 类型。
const a = { x: 0 };
type T0 = typeof a; // { x: number }
type T1 = typeof a.x; // number
上面示例中,typeof a
表示返回变量a
的 TypeScript 类型({ x: number }
)。同理,typeof a.x
返回的是属性x
的类型(number
)。
这种用法的typeof
返回的是 TypeScript 类型,所以只能用在类型运算之中(即跟类型相关的代码之中),不能用在值运算。
type T = typeof Date(); // 报错
上面示例会报错,原因是 typeof 的参数不能是一个值的运算式,而Date()
需要运算才知道结果。
另外,typeof
命令的参数不能是类型。
type Age = number;
type MyAge = typeof Age; // 报错
上面示例中,Age
是一个类型别名,用作typeof
命令的参数就会报错。
typeof 是一个很重要的 TypeScript 运算符,有些场合不知道某个变量foo
的类型,这时使用typeof foo
就可以获得它的类型。
9.块级类型声明
TypeScript 支持块级类型声明,即类型可以声明在代码块(用大括号表示)里面,并且只在当前代码块有效。
if (true) {
type T = number;
let v:T = 5;
} else {
type T = string;
let v:T = 'hello';
}
10.类型的兼容
type T = number|string;
let a:number = 1;
let b:T = a;
变量a
和b
的类型是不一样的,但是变量a
赋值给变量b
并不会报错。这时,我们就认为,b
的类型兼容a
的类型。
如果类型A
的值可以赋值给类型B
,那么类型A
就称为类型B
的子类型(subtype)。在上例中,类型number
就是类型number|string
的子类型
let a:'hi' = 'hi';
let b:string = 'hello';
b = a; // 正确
a = b; // 报错
上面示例中,hi
是string
的子类型,string
是hi
的父类型。所以,变量a
可以赋值给变量b
,但是反过来就会报错。
子类型继承了父类型的所有特征,所以可以用在父类型的场合。但是,子类型还可能有一些父类型没有的特征,所以父类型不能用在子类型的场合。
四、数组
TypeScript 数组有一个根本特征:所有成员的类型必须相同,但是成员数量是不确定的,可以是无限数量的成员,也可以是零成员
声明的方式:
let arr:number[] = [1, 2, 3];
let arr:(number|string)[];
let arr:Array<number> = [1, 2, 3];
数组类型声明了以后,成员数量是不限制的,任意数量的成员都可以,也可以是空数组。
let arr:number[];
arr = [];
arr = [1];
arr = [1, 2];
arr = [1, 2, 3];
let arr1:number[] = [1, 2, 3];
arr1[3] = 4;
arr1.length = 2;
arr1// [1, 2]
上面示例中,数组arr
无论有多少个成员,都是正确的。
这种规定的隐藏含义就是,数组的成员是可以动态变化的,数组增加成员或减少成员,都是可以的。
1.类型推断
如果数组变量没有声明类型,TypeScript 就会推断数组成员的类型。这时,推断行为会因为值的不同,而有所不同。后面,为这个数组赋值时,TypeScript 会自动更新类型推断。
const arr = [];
arr // 推断为 any[]
arr.push(123);
arr // 推断类型为 number[]
arr.push('abc');
arr // 推断类型为 (string|number)[]
但是,类型推断的自动更新只发生初始值为空数组的情况。如果初始值不是空数组,类型推断就不会更新。
// 推断类型为 number[]
const arr = [123];
arr.push('abc'); // 报错
上面示例中,数组变量arr
的初始值是[123]
,TypeScript 就推断成员类型为number
。新成员如果不是这个类型,TypeScript 就会报错,而不会更新类型推断。
2.只读数组,const断言
TypeScript 允许声明只读数组,方法是在数组类型前面加上readonly
关键字。arr
是一个只读数组,删除、修改、新增数组成员都会报错。
const arr:readonly number[] = [0, 1];
arr[1] = 2; // 报错
arr.push(3); // 报错
delete arr[0]; // 报错
TypeScript 将readonly number[]
与number[]
视为两种不一样的类型,后者是前者的子类型。
所以子类型number[]
可以用于所有使用父类型的场合,反过来就不行。
let a1:number[] = [0, 1];
let a2:readonly number[] = a1; // 正确
a1 = a2; // 报错
只读数组还有一种声明方法,就是使用“const 断言”。
const arr = [0, 1] as const;
arr[0] = [2]; // 报错
上面示例中,as const
告诉 TypeScript,推断类型时要把变量arr
推断为只读数组,从而使得数组成员无法改变。
3.多维数组
var multi:number[][] =
[[1,2,3], [23,24,25]];
五、元组
它表示成员类型可以自由设置的数组,即数组的各个成员的类型可以不同
数组的成员类型写在方括号外面(number[]
),元组的成员类型是写在方括号里面([number]
// 数组
let a:number[] = [1];
// 元组
let t:[number] = [1];
使用元组时,必须明确给出类型声明(上例的[number]
),不能省略,否则 TypeScript 会把一个值自动推断为数组。
// a 的类型被推断为 (number | boolean)[]
let a = [1, true];
元组成员的类型可以添加问号后缀(?
),表示该成员是可选的。所有可选成员必须在必选成员之后。
let a:[number, number?] = [1];
由于需要声明每个成员的类型,所以大多数情况下,元组的成员数量是有限的,从类型声明就可以明确知道,元组包含多少个成员,越界的成员会报错。
let x:[string, string] = ['a', 'b'];
x[2] = 'c'; // 报错
但是,使用扩展运算符(...
),可以表示不限成员数量的元组。
type NamedNums = [
string,
...number[]
];
const a:NamedNums = ['A', 1, 2];
const b:NamedNums = ['B', 1, 2, 3];
1.只读元组
元组也可以是只读的,不允许修改,有两种写法。
// 写法一
type t = readonly [number, string]
// 写法二
type t = Readonly<[number, string]>
跟数组一样,只读元组是元组的父类型。所以,元组可以替代只读元组,而只读元组不能替代元组。
type t1 = readonly [number, number];
type t2 = [number, number];
let x:t2 = [1, 2];
let y:t1 = x; // 正确
x = y; // 报错
2、成员数量推断
function f(point: [number, number]) {
if (point.length === 3) { // 报错
// ...
}
}
上面示例会报错,原因是 TypeScript 发现元组point
的长度是2
,不可能等于3
,这个判断无意义
如果包含了可选成员,TypeScript 会推断出可能的成员数量。
function f(
point:[number, number?, number?]
) {
if (point.length === 4) { // 报错
// ...
}
}
如果使用了扩展运算符,TypeScript 就无法推断出成员数量。
const myTuple:[...string[]]
= ['a', 'b', 'c'];
if (myTuple.length === 4) { // 正确
// ...
}
一旦扩展运算符使得元组的成员数量无法推断,TypeScript 内部就会把该元组当成数组处理
3.扩展运算符与成员数量
如果函数调用时,使用扩展运算符传入函数参数,可能发生参数数量与数组长度不匹配的报错。
const arr = [1, 2];
function add(x:number, y:number){
// ...
}
add(...arr) // 报错
解决这个问题的一个方法,就是把成员数量不确定的数组,写成成员数量确定的元组,再使用扩展运算符。
const arr:[number, number] = [1, 2];
function add(x:number, y:number){
// ...
}
add(...arr) // 正确
六、symbol类型
Symbol 值通过Symbol()
函数生成。在 TypeScript 里面,Symbol 的类型使用symbol
表示。
let x:symbol = Symbol();
let y:symbol = Symbol();
x === y // false
上面示例中,变量x
和y
的类型都是symbol
,且都用Symbol()
生成,但是它们是不相等的。
1.unique symbol
symbol
的一个子类型unique symbol
,它表示单个的、某个具体的 Symbol 值。
因为unique symbol
表示单个值,所以这个类型的变量是不能修改值的,只能用const
命令声明,不能用let
声明。
// 正确
const x:unique symbol = Symbol();
// 报错
let y:unique symbol = Symbol();
const x:unique symbol = Symbol();
// 等同于
const x = Symbol();
每个声明为unique symbol
类型的变量,它们的值都是不一样的,其实属于两个值类型。
const a:unique symbol = Symbol();
const b:unique symbol = Symbol();
a === b // 报错
变量a
和b
是两个类型,就不能把一个赋值给另一个。
const a:unique symbol = Symbol();
const b:unique symbol = a; // 报错
如果要写成与变量a
同一个unique symbol
值类型,只能写成类型为typeof a
。
const a:unique symbol = Symbol();
const b:typeof a = a; // 正确
unique symbol 类型是 symbol 类型的子类型,所以可以将前者赋值给后者,但是反过来就不行。
const a:unique symbol = Symbol();
const b:symbol = a; // 正确
const c:unique symbol = b; // 报错
2.类型推断
let
命令声明的变量,推断类型为 symbol。
// 类型为 symbol
let x = Symbol();
const
命令声明的变量,推断类型为 unique symbol。
// 类型为 unique symbol
const x = Symbol();
但是,const
命令声明的变量,如果赋值为另一个 symbol 类型的变量,则推断类型为 symbol。
let x = Symbol();
// 类型为 symbol
const y = x;
let
命令声明的变量,如果赋值为另一个 unique symbol 类型的变量,则推断类型还是 symbol。
const x = Symbol();
// 类型为 symbol
let y = x;
七、函数类型
数的类型声明,需要在声明函数时,给出参数的类型和返回值的类型。
function hello(
txt:string
):void {
console.log('hello ' + txt);
}
上面示例中,函数hello()
在声明时,需要给出参数txt
的类型(string),以及返回值的类型(void
),后者写在参数列表的圆括号后面。void
类型表示没有返回值(返回值的类型通常可以不写,因为 TypeScript 自己会推断出来。)
如果变量被赋值为一个函数,变量的类型有两种写法。
// 写法一
const hello = function (txt:string) {
console.log('hello ' + txt);
}
// 写法二
const hello:
(txt:string) => void
= function (txt) {
console.log('hello ' + txt);
};
如果函数的类型定义很冗长,或者多个函数使用同一种类型,写法二用起来就很麻烦。因此,往往用type
命令为函数类型定义一个别名,便于指定给其他变量。
type MyFunc = (txt:string) => void;
const hello:MyFunc = function (txt) {
console.log('hello ' + txt);
};
函数的实际参数个数,可以少于类型指定的参数个数,但是不能多于,即 TypeScript 允许省略参数。
let myFunc:
(a:number, b:number) => number;
myFunc = (a:number) => a; // 正确
myFunc = (
a:number, b:number, c:number
) => a + b + c; // 报错
如果一个变量要套用另一个函数类型,有一个小技巧,就是使用typeof
运算符。
function add(
x:number,
y:number
) {
return x + y;
}
const myAdd:typeof add = function (x, y) {
return x + y;
}
1.Function类型
TypeScript 提供 Function 类型表示函数,任何函数都属于这个类型。(不建议这样使用,不然传入和传出的都是any类型)
function doSomething(f:Function) {
return f(1, 2, 3);
}
2.箭头函数
箭头函数是普通函数的一种简化写法,它的类型写法与普通函数类似。
const repeat = (
str:string,
times:number
):string => str.repeat(times);
看一个例子。
type Person = { name: string };
const people = ['alice', 'bob', 'jan'].map(
(name):Person => ({name})
);
上面示例中,Person
是一个类型别名,代表一个对象,该对象有属性name
。变量people
是数组的map()
方法的返回值。
map()
方法的参数是一个箭头函数(name):Person => ({name})
,该箭头函数的参数name
的类型省略了,因为可以从map()
的类型定义推断出来,箭头函数的返回值类型为Person
。相应地,变量people
的类型是Person[]
。
至于箭头后面的({name})
,表示返回一个对象,该对象有一个属性name
,它的属性值为变量name
的值。这里的圆括号是必须的,否则(name):Person => {name}
的大括号表示函数体,即函数体内有一行语句name
,同时由于没有return
语句,这个函数不会返回任何值。
3.可选参数
如果函数的某个参数可以省略,则在参数名后面加问号表示。(参数名带有问号,表示该参数的类型实际上是原始类型|undefined
,它有可能为undefined
)
function f(x?:number) {
// ...
}
f(); // OK
f(10); // OK
函数体内部用到可选参数时,需要判断该参数是否为undefined
。
let myFunc:
(a:number, b?:number) => number;
myFunc = function (x, y) {
if (y === undefined) {
return x;
}
return x + y;
}
上面示例中,由于函数的第二个参数为可选参数,所以函数体内部需要判断一下,该参数是否为空。
4.参数默认值
设置了默认值的参数,就是可选的。如果不传入该参数,它就会等于默认值。(可选参数与默认值不能同时使用。)
function createPoint(
x:number = 0,
y:number = 0
):[number, number] {
return [x, y];
}
createPoint() // [0, 0]
具有默认值的参数如果不位于参数列表的末尾,调用时不能省略,如果要触发默认值,必须显式传入undefined
。
function add(
x:number = 0,
y:number
) {
return x + y;
}
add(1) // 报错
add(undefined, 1) // 正确
5.参数解构
函数参数如果存在变量解构,类型写法如下。
function f(
[x, y]: [number, number]
) {
// ...
}
function sum(
{ a, b, c }: {
a: number;
b: number;
c: number
}
) {
console.log(a + b + c);
}
参数解构可以结合类型别名(type 命令)一起使用,代码会看起来简洁一些。
type ABC = { a:number; b:number; c:number };
function sum({ a, b, c }:ABC) {
console.log(a + b + c);
}
6.rest参数
rest 参数表示函数剩余的所有参数,它可以是数组(剩余参数类型相同),也可能是元组(剩余参数类型不同)。
// rest 参数为数组
function joinNumbers(...nums:number[]) {
// ...
}
// rest 参数为元组
function f(...args:[boolean, number]) {
// ...
}
下面是一个 rest 参数的例子。
function multiply(n:number, ...m:number[]) {
return m.map((x) => n * x);
}
上面示例中,参数m
就是 rest 类型,它的类型是一个数组。
rest 参数甚至可以嵌套。
function f(...args:[boolean, ...string[]]) {
// ...
}
rest 参数可以与变量解构结合使用。
function repeat(
...[str, times]: [string, number]
):string {
return str.repeat(times);
}
// 等同于
function repeat(
str: string,
times: number
):string {
return str.repeat(times);
}
7.readonly只读参数
如果函数内部不能修改某个参数,可以在函数定义时,在参数类型前面加上readonly
关键字,表示这是只读参数。
function arraySum(
arr:readonly number[]
) {
// ...
arr[0] = 0; // 报错
}
8.void类型
void 类型表示函数没有返回值。(如果返回其他值,就会报错,但是允许返回null和undefined,如果打开了strictNullChecks
编译选项,那么 void 类型只允许返回undefined
。如果返回null
,就会报错。这是因为 JavaScript 规定,如果函数没有返回值,就等同于返回undefined
。)
function f():void {
console.log('hello');
}
需要特别注意的是,如果变量、对象方法、函数参数的类型是 void 类型的函数,那么并不代表不能赋值为有返回值的函数。恰恰相反,该变量、对象方法和函数参数可以接受返回任意值的函数,这时并不会报错。
type voidFunc = () => void;
const f:voidFunc = () => {
return 123;
};
这是因为,这时 TypeScript 认为,这里的 void 类型只是表示该函数的返回值没有利用价值,或者说不应该使用该函数的返回值。只要不用到这里的返回值,就不会报错。
const src = [1, 2, 3];
const ret = [];
src.forEach(el => ret.push(el));
上面示例中,push()
有返回值,表示新插入的元素在数组里面的位置。但是,对于forEach()
方法来说,这个返回值是没有作用的,根本用不到,所以 TypeScript 不会报错。
9.never类型
never
类型表示肯定不会出现的值。它用在函数的返回值,就表示某个函数肯定不会返回值,即函数不会正常执行结束。
它主要有以下两种情况。
(1)抛出错误的函数。
function fail(msg:string):never {
throw new Error(msg);
}
上面示例中,函数fail()
会抛错,不会正常退出,所以返回值类型是never
。
注意,只有抛出错误,才是 never 类型。如果显式用return
语句返回一个 Error 对象,返回值就不是 never 类型。
function fail():Error {
return new Error("Something failed");
}
上面示例中,函数fail()
返回一个 Error 对象,所以返回值类型是 Error。
(2)无限执行的函数。
const sing = function():never {
while (true) {
console.log('sing');
}
};
注意,never
类型不同于void
类型。前者表示函数没有执行结束,不可能有返回值;后者表示函数正常执行结束,但是不返回值,或者说返回undefined
。
如果程序中调用了一个返回值类型为never
的函数,那么就意味着程序会在该函数的调用位置终止,永远不会继续执行后续的代码。
10.局部类型
函数内部允许声明其他类型,该类型只在函数内部有效,称为局部类型。
function hello(txt:string) {
type message = string;
let newTxt:message = 'hello ' + txt;
return newTxt;
}
const newTxt:message = hello('world'); // 报错
上面示例中,类型message
是在函数hello()
内部定义的,只能在函数内部使用。在函数外部使用,就会报错。
11.函数重载
有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)。
reverse('abc') // 'cba'
reverse([1, 2, 3]) // [3, 2, 1]
分别对函数reverse()
的两种参数情况,给予了类型声明。但是,到这里还没有结束,后面还必须对函数reverse()
给予完整的类型声明。
function reverse(str:string):string;
function reverse(arr:any[]):any[];
function reverse(
stringOrArray:string|any[]
):string|any[] {
if (typeof stringOrArray === 'string')
return stringOrArray.split('').reverse().join('');
else
return stringOrArray.slice().reverse();
}
函数体内部需要判断参数的类型及个数,并根据判断结果执行不同的操作。
function add(
x:number,
y:number
):number;
function add(
x:any[],
y:any[]
):any[];
function add(
x:number|any[],
y:number|any[]
):number|any[] {
if (typeof x === 'number' && typeof y === 'number') {
return x + y;
} else if (Array.isArray(x) && Array.isArray(y)) {
return [...x, ...y];
}
throw new Error('wrong parameters');
}
重载声明的排序很重要,因为 TypeScript 是按照顺序进行检查的,一旦发现符合某个类型声明,就不再往下检查了,所以类型最宽的声明应该放在最后面,防止覆盖其他类型声明。
function f(x:any):number;
function f(x:string): 0|1;
function f(x:any):any {
// ...
}
const a:0|1 = f('hi'); // 报错
上面声明中,第一行类型声明x:any
范围最宽,导致函数f()
的调用都会匹配这行声明,无法匹配第二行类型声明,所以最后一行调用就报错了,因为等号两侧类型不匹配,左侧类型是0|1
,右侧类型是number
。这个函数重载的正确顺序是,第二行类型声明放到第一行的位置
八、对象
1.可选属性
如果某个属性是可选的(即可以忽略),需要在属性名后面加一个问号。(可选属性等同于允许赋值为undefined
)
const obj: {
x: number;
y?: number;
} = { x: 1 };
/ 写法一
let firstName = (user.firstName === undefined)
? 'Foo' : user.firstName;
let lastName = (user.lastName === undefined)
? 'Bar' : user.lastName;
// 写法二
let firstName = user.firstName ?? 'Foo';
let lastName = user.lastName ?? 'Bar';
上面示例中,写法一使用三元运算符?:
,判断是否为undefined
,并设置默认值。写法二使用 Null 判断运算符??
,与写法一的作用完全相同。
TypeScript 提供编译设置ExactOptionalPropertyTypes
,只要同时打开这个设置和strictNullChecks
,可选属性就不能设为undefined
。
// 打开 ExactOptionsPropertyTypes 和 strictNullChecks
const obj: {
x: number;
y?: number;
} = { x: 1, y: undefined }; // 报错
上面示例中,打开了这两个设置以后,可选属性就不能设为undefined
了。
注意,可选属性与允许设为undefined
的必选属性是不等价的。
type A = { x:number, y?:number };
type B = { x:number, y:number|undefined };
const ObjA:A = { x: 1 }; // 正确
const ObjB:B = { x: 1 }; // 报错
上面示例中,属性y
如果是一个可选属性,那就可以省略不写;如果是允许设为undefined
的必选属性,一旦省略就会报错,必须显式写成{ x: 1, y: undefined }
2.只读属性
属性名前面加上readonly
关键字,表示这个属性是只读属性,不能修改。
interface MyInterface {
readonly prop: number;
}
注意,如果属性值是一个对象,readonly
修饰符并不禁止修改该对象的属性,只是禁止完全替换掉该对象。
interface Home {
readonly resident: {
name: string;
age: number
};
}
const h:Home = {
resident: {
name: 'Vicky',
age: 42
}
};
h.resident.age = 32; // 正确
h.resident = {
name: 'Kate',
age: 23
} // 报错
如果希望属性值是只读的,除了声明时加上readonly
关键字,还有一种方法,就是在赋值时,在对象后面加上只读断言as const
。
const myUser = {
name: "Sabrina",
} as const;
myUser.name = "Cynthia"; // 报错
注意,上面的as const
属于 TypeScript 的类型推断,如果变量明确地声明了类型,那么 TypeScript 会以声明的类型为准。
const myUser:{ name: string } = {
name: "Sabrina",
} as const;
myUser.name = "Cynthia"; // 正确
3.属性名的索引类型
type MyObj = {
[property: string]: string
};
const obj:MyObj = {
foo: 'a',
bar: 'b',
baz: 'c',
};
上面示例中,类型MyObj
的属性名类型就采用了表达式形式,写在方括号里面。[property: string]
的property
表示属性名,这个是可以随便起的,它的类型是string
,即属性名类型为string
。也就是说,不管这个对象有多少属性,只要属性名为字符串,且属性值也是字符串,就符合这个类型声明。
4.解构赋值
解构赋值用于直接从对象中提取属性。
const {id, name, price} = product;
上面语句从对象product
提取了三个属性,并声明属性名的同名变量。
5.结构类型原则
只要对象 B 满足 对象 A 的结构特征,TypeScript 就认为对象 B 兼容对象 A 的类型,这称为“结构类型”原则(structural typing)。
type A = {
x: number;
};
type B = {
x: number;
y: number;
};
对象A
只有一个属性x
,类型为number
。对象B
满足这个特征,因此兼容对象A
,只要可以使用A
的地方,就可以使用B
。
const B = {
x: 1,
y: 1
};
const A:{ x: number } = B; // 正确
九、interface
1.interface对象的5种语法
- 对象属性
- 对象的属性索引
- 对象方法
- 函数
- 构造函数
(1)对象属性
interface Point {
x: number;
y: number;
}
上面示例中,x
和y
都是对象的属性,分别使用冒号指定每个属性的类型。
(2)对象的属性索引
interface A {
[prop: string]: number;
}
上面示例中,[prop: string]
就是属性的字符串索引,表示属性名只要是字符串,都符合类型要求。
3)对象的方法
对象的方法共有三种写法。
// 写法一
interface A {
f(x: boolean): string;
}
// 写法二
interface B {
f: (x: boolean) => string;
}
// 写法三
interface C {
f: { (x: boole
(4)函数
interface 也可以用来声明独立的函数。
interface Add {
(x:number, y:number): number;
}
const myAdd:Add = (x,y) => x + y;
上面示例中,接口Add
声明了一个函数类型。
(5)构造函数
interface 内部可以使用new
关键字,表示构造函数。
interface ErrorConstructor {
new (message?: string): Error;
}
2.interface的继承
2.1.interface继承interface
interface 可以使用extends
关键字,继承其他 interface。
interface Shape {
name: string;
}
interface Circle extends Shape {
radius: number;
}
上面示例中,Circle
继承了Shape
,所以Circle
其实有两个属性name
和radius
。这时,Circle
是子接口,Shape
是父接口。
extends
关键字会从继承的接口里面拷贝属性类型,这样就不必书写重复的属性。
interface 允许多重继承。
interface Style {
color: string;
}
interface Shape {
name: string;
}
interface Circle extends Style, Shape {
radius: number;
}
上面示例中,Circle
同时继承了Style
和Shape
,所以拥有三个属性color
、name
和radius
。
多重接口继承,实际上相当于多个父接口的合并
2.2interface继承type
nterface 可以继承type
命令定义的对象类型。
type Country = {
name: string;
capital: string;
}
interface CountryWithPop extends Country {
population: number;
}
上面示例中,CountryWithPop
继承了type
命令定义的Country
对象,并且新增了一个population
属性。
注意,如果type
命令定义的类型不是对象,interface 就无法继承。
2.3interface继承class
nterface 还可以继承 class,即继承该类的所有成员。
class A {
x:string = '';
y():boolean {
return true;
}
}
interface B extends A {
z: number
}
上面示例中,B
继承了A
,因此B
就具有属性x
、y()
和z
。
实现B
接口的对象就需要实现这些属性。
const b:B = {
x: '',
y: function(){ return true },
z: 123
}
上面示例中,对象b
就实现了接口B
,而接口B
又继承了类A
。
3.接口合并
多个同名接口会合并成一个接口。
interface Box {
height: number;
width: number;
}
interface Box {
length: number;
}
上面示例中,两个Box
接口会合并成一个接口,同时有height
、width
和length
三个属性。
4.interface与type的异同
interface
命令与type
命令作用类似,都可以表示对象类型。
很多对象类型既可以用 interface 表示,也可以用 type 表示。而且,两者往往可以换用,几乎所有的 interface 命令都可以改写为 type 命令。
它们的相似之处,首先表现在都能为对象类型起名。
type Country = {
name: string;
capital: string;
}
interface Coutry {
name: string;
capital: string;
}
上面示例是type
命令和interface
命令,分别定义同一个类型。
4.1interface与type的区别:
(1)type
能够表示非对象类型,而interface
只能表示对象类型(包括数组、函数等)。
(2)interface
可以继承其他类型,type
不支持继承。
继承的主要作用是添加属性,type
定义的对象类型如果想要添加属性,只能使用&
运算符,重新定义一个类型。
type Animal = {
name: string
}
type Bear = Animal & {
honey: boolean
}
上面示例中,类型Bear
在Animal
的基础上添加了一个属性honey
。上例的&
运算符,表示同时具备两个类型的特征,因此可以起到两个对象类型合并的作用。
作为比较,interface
添加属性,采用的是继承的写法。
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
}
继承时,type 和 interface 是可以换用的。interface 可以继承 type。
type Foo = { x: number; };
interface Bar extends Foo {
y: number;
}
type 也可以继承 interface。
interface Foo {
x: number;
}
type Bar = Foo & { y: number; };
(3)同名interface
会自动合并,同名type
则会报错。也就是说,TypeScript 不允许使用type
多次定义同一个类型。
type A = { foo:number }; // 报错
type A = { bar:number }; // 报错
上面示例中,type
两次定义了类型A
,导致两行都会报错。
(4)interface
不能包含属性映射(mapping),type
可以
interface Point {
x: number;
y: number;
}
// 正确
type PointCopy1 = {
[Key in keyof Point]: Point[Key];
};
// 报错
interface PointCopy2 {
[Key in keyof Point]: Point[Key];
};
(5)this
关键字只能用于interface
。
// 正确
interface Foo {
add(num:number): this;
};
// 报错
type Foo = {
add(num:number): this;
};
6)type 可以扩展原始数据类型,interface 不行。
// 正确
type MyStr = string & {
type: 'new'
};
// 报错
interface MyStr extends string {
type: 'new'
}
(7)interface
无法表达某些复杂类型(比如交叉类型和联合类型),但是type
可以。
type A = { /* ... */ };
type B = { /* ... */ };
type AorB = A | B;
type AorBwithName = AorB & {
name: string
};
上面示例中,类型AorB
是一个联合类型,AorBwithName
则是为AorB
添加一个属性。这两种运算,interface
都没法表达。
综上所述,如果有复杂的类型运算,那么没有其他选择只能使用type
;一般情况下,interface
灵活性比较高,便于扩充类型或自动合并,建议优先使用
十、类
1.基本使用
1.1属性的类型
类的属性可以在顶层声明,也可以在构造方法内部声明。
对于顶层声明的属性,可以在声明时同时给出类型。
class Point {
x:number;
y:number;
}
1.2.readonly修饰符
属性名前面加上 readonly 修饰符,就表示该属性是只读的。实例对象不能修改这个属性。
class A {
readonly id = 'foo';
}
const a = new A();
a.id = 'bar'; // 报错
1.3 方法的类型
类的方法就是普通函数,类型声明方式与函数一致。
class Point {
x:number;
y:number;
constructor(x:number, y:number) {
this.x = x;
this.y = y;
}
add(point:Point) {
return new Point(
this.x + point.x,
this.y + point.y
);
}
}
上面示例中,构造方法constructor()
和普通方法add()
都注明了参数类型,但是省略了返回值类型,因为 TypeScript 可以自己推断出来。
1.4.存取器的方法
存取器(accessor)是特殊的类方法,包括取值器(getter)和存值器(setter)两种方法。
它们用于读写某个属性,取值器用来读取属性,存值器用来写入属性。
class C {
_name = '';
get name() {
return this._name;
}
set name(value) {
this._name = value;
}
}
上面示例中,get name()
是取值器,其中get
是关键词,name
是属性名。外部读取name
属性时,实例对象会自动调用这个方法,该方法的返回值就是name
属性的值。
TypeScript 对存取器有以下规则。
(1)如果某个属性只有get
方法,没有set
方法,那么该属性自动成为只读属性。
class C {
_name = 'foo';
get name() {
return this._name;
}
}
const c = new C();
c.name = 'bar'; // 报错
上面示例中,name
属性没有set
方法,对该属性赋值就会报错。
(2)TypeScript 5.1 版之前,set
方法的参数类型,必须兼容get
方法的返回值类型,否则报错。
// TypeScript 5.1 版之前
class C {
_name = '';
get name():string { // 报错
return this._name;
}
set name(value:number) {
this._name = String(value);
}
}
上面示例中,get
方法的返回值类型是字符串,与set
方法的参数类型number
不兼容,导致报错。改成下面这样,就不会报错。
class C {
_name = '';
get name():string {
return this._name;
}
set name(value:number|string) {
this._name = String(value);
}
}
上面示例中,set
方法的参数类型(number|string
)兼容get
方法的返回值类型(string
),这是允许的。
TypeScript 5.1 版做出了改变,现在两者可以不兼容。
(3)get
方法与set
方法的可访问性必须一致,要么都为公开方法,要么都为私有方法。
1.5属性索引
类允许定义属性索引。
class MyClass {
[s:string]: boolean |
((s:string) => boolean);
get(s:string) {
return this[s] as boolean;
}
}
上面示例中,[s:string]
表示所有属性名类型为字符串的属性,它们的属性值要么是布尔值,要么是返回布尔值的函数。
2.类的interface接口
2.1implements关键字
类使用 implements 关键字,表示当前类满足这些外部类型条件的限制
interface Country {
name:string;
capital:string;
}
// 或者
type Country = {
name:string;
capital:string;
}
class MyCountry implements Country {
name = '';
capital = '';
}
上面示例中,interface
或type
都可以定义一个对象类型。类MyCountry
使用implements
关键字,表示该类的实例对象满足这个外部类型。
2.2实现多个接口
类可以实现多个接口(其实是接受多重限制),每个接口之间使用逗号分隔。
class Car implements MotorVehicle, Flyable, Swimmable {
// ...
}
但是,同时实现多个接口并不是一个好的写法,容易使得代码难以管理,可以使用两种方法替代。
第一种方法是类的继承
class Car implements MotorVehicle {
}
class SecretCar extends Car implements Flyable, Swimmable {
}
第二种方法是接口的继承
interface A {
a:number;
}
interface B extends A {
b:number;
}
可以用接口继承改写
interface MotorVehicle {
// ...
}
interface Flyable {
// ...
}
interface Swimmable {
// ...
}
interface SuperCar extends MotoVehicle,Flyable, Swimmable {
// ...
}
class SecretCar implements SuperCar {
// ...
}
2.3类与接口的合并
TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。
class A {
x:number = 1;
}
interface A {
y:number;
}
let a = new A();
a.y = 10;
a.x // 1
a.y // 10
注意,合并进类的非空属性(上例的y
),如果在赋值之前读取,会返回undefined
。
class A {
x:number = 1;
}
interface A {
y:number;
}
let a = new A();
a.y // undefined
3.class类型
3.1实例类型
TypeScript 的类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型。
class Color {
name:string;
constructor(name:string) {
this.name = name;
}
}
const green:Color = new Color('green');
上面示例中,定义了一个类Color
。它的类名就代表一种类型,实例对象green
就属于该类型。
对于引用实例对象的变量来说,既可以声明类型为 Class,也可以声明类型为 Interface,因为两者都代表实例对象的类型。
interface MotorVehicle {
}
class Car implements MotorVehicle {
}
// 写法一
const c1:Car = new Car();
// 写法二
const c2:MotorVehicle = new Car();
上面示例中,变量的类型可以写成类Car
,也可以写成接口MotorVehicle
。它们的区别是,如果类Car
有接口MotoVehicle
没有的属性和方法,那么只有变量c1
可以调用这些属性和方法。
3.2类的自身类型
要获得一个类的自身类型,一个简便的方法就是使用 typeof 运算符。
function createPoint(
PointClass:typeof Point,
x:number,
y:number
):Point {
return new PointClass(x, y);
}
上面示例中,createPoint()
的第一个参数PointClass
是Point
类自身,要声明这个参数的类型,简便的方法就是使用typeof Point
。因为Point
类是一个值,typeof Point
返回这个值的类型。注意,createPoint()
的返回值类型是Point
,代表实例类型。
3.3结构类型原则
Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型。
class Foo {
id!:number;
}
function fn(arg:Foo) {
// ...
}
const bar = {
id: 10,
amount: 100,
};
fn(bar); // 正确
上面示例中,对象bar
满足类Foo
的实例结构,只是多了一个属性amount
。所以,它可以当作参数,传入函数fn()
。
注意,确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法。
class Point {
x: number;
y: number;
static t: number;
constructor(x:number) {}
}
class Position {
x: number;
y: number;
z: number;
constructor(x:string) {}
}
const point:Point = new Position('');
上面示例中,Point
与Position
的静态属性和构造方法都不一样,但因为Point
的实例成员与Position
相同,所以Position
兼容Point
。
如果类中存在私有成员(private)或保护成员(protected),那么确定兼容关系时,TypeScript 要求私有成员和保护成员来自同一个类,这意味着两个类需要存在继承关系。
// 情况一
class A {
private name = 'a';
}
class B extends A {
}
const a:A = new B();
// 情况二
class A {
protected name = 'a';
}
class B extends A {
protected name = 'b';
}
const a:A = new B();
上面示例中,A
和B
都有私有成员(或保护成员)name
,这时只有在B
继承A
的情况下(class B extends A
),B
才兼容A
4.类的继承
类(这里又称“子类”)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法。
class A {
greet() {
console.log('Hello, world!');
}
}
class B extends A {
}
const b = new B();
b.greet() // "Hello, world!"
上面示例中,子类B
继承了基类A
,因此就拥有了greet()
方法,不需要再次在类的内部定义这个方法了。
如果基类包括保护成员(protected
修饰符),子类可以将该成员的可访问性设置为公开(public
修饰符),也可以保持保护成员不变,但是不能改用私有成员(private
修饰符)
class A {
protected x: string = '';
protected y: string = '';
protected z: string = '';
}
class B extends A {
// 正确
public x:string = '';
// 正确
protected y:string = '';
// 报错
private z: string = '';
}
上面示例中,子类B
将基类A
的受保护成员改成私有成员,就会报错。
如果基类包括保护成员(protected
修饰符),子类可以将该成员的可访问性设置为公开(public
修饰符),也可以保持保护成员不变,但是不能改用私有成员(private
修饰符),详见后文。
class A {
protected x: string = '';
protected y: string = '';
protected z: string = '';
}
class B extends A {
// 正确
public x:string = '';
// 正确
protected y:string = '';
// 报错
private z: string = '';
}
上面示例中,子类B
将基类A
的受保护成员改成私有成员,就会报错
对于那些只设置了类型、没有初值的顶层属性,有一个细节需要注意。
interface Animal {
animalStuff: any;
}
interface Dog extends Animal {
dogStuff: any;
}
class AnimalHouse {
resident: Animal;
constructor(animal:Animal) {
this.resident = animal;
}
}
class DogHouse extends AnimalHouse {
resident: Dog;
constructor(dog:Dog) {
super(dog);
}
}
上面示例中,类DogHouse
的顶层成员resident
只设置了类型(Dog
),没有设置初值。这段代码在不同的编译设置下,编译结果不一样。
5.可访问性修饰符
类的内部成员的外部可访问性,由三个可访问性修饰符(access modifiers)控制:public
、private
和protected
。
这三个修饰符的位置,都写在属性或方法的最前面
5.1public
public
修饰符表示这是公开成员,外部可以自由访问。
class Greeter {
public greet() {
console.log("hi!");
}
}
const g = new Greeter();
g.greet();
上面示例中,greet()
方法前面的public
修饰符,表示该方法可以在类的外部调用,即外部实例可以调用。
public
修饰符是默认修饰符,如果省略不写,实际上就带有该修饰符。因此,类的属性和方法默认都是外部可访问的。
正常情况下,除非为了醒目和代码可读性,public
都是省略不写的
5.2 private
private
修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。
class A {
private x:number = 0;
}
const a = new A();
a.x // 报错
class B extends A {
showX() {
console.log(this.x); // 报错
}
}
上面示例中,属性x
前面有private
修饰符,表示这是私有成员。因此,实例对象和子类使用该成员,都会报错。
由于private
存在这些问题,加上它是 ES2022 标准发布前出台的,而 ES2022 引入了自己的私有成员写法#propName
。因此建议不使用private
,改用 ES2022 的写法,获得真正意义的私有成员。
class A {
#x = 1;
}
const a = new A();
a['x'] // 报错
上面示例中,采用了 ES2022 的私有成员写法(属性名前加#
),TypeScript 就正确识别了实例对象没有属性x
,从而报错。
一般会有一个静态方法,充当工厂函数,强制所有实例都通过该方法生成。
class Singleton {
private static instance?: Singleton;
private constructor() {}
static getInstance() {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
const s = Singleton.getInstance();
上面示例使用私有构造方法,实现了单例模式。想要获得 Singleton 的实例,不能使用new
命令,只能使用getInstance()
方法。
5.3protected
protected
修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。
class A {
protected x = 1;
}
class B extends A {
getX() {
return this.x;
}
}
const a = new A();
const b = new B();
a.x // 报错
b.getX() // 1
上面示例中,类A
的属性x
是保护成员,直接从实例读取该属性(a.x
)会报错,但是子类B
内部可以读取该属性。
在类的外部,实例对象不能读取保护成员,但是在类的内部可以。
class A {
protected x = 1;
f(obj:A) {
console.log(obj.x);
}
}
const a = new A();
a.x // 报错
a.f(a) // 1
上面示例中,属性x
是类A
的保护成员,在类的外部,实例对象a
拿不到这个属性。但是,实例对象a
传入类A
的内部,就可以从a
拿到x
。
5.4示例属性的简写形式
实际开发中,很多实例属性的值,是通过构造方法传入的。
class Point {
x:number;
y:number;
constructor(x:number, y:number) {
this.x = x;
this.y = y;
}
}
上面实例中,属性x
和y
的值是通过构造方法的参数传入的。
这样的写法等于对同一个属性要声明两次类型,一次在类的头部,另一次在构造方法的参数里面。这有些累赘,TypeScript 就提供了一种简写形式。
class Point {
constructor(
public x:number,
public y:number
) {}
}
const p = new Point(10, 10);
p.x // 10
p.y // 10
上面示例中,构造方法的参数x
前面有public
修饰符,这时 TypeScript 就会自动声明一个公开属性x
,不必在构造方法里面写任何代码,同时还会设置x
的值为构造方法的参数值。注意,这里的public
不能省略。
除了public
修饰符,构造方法的参数名只要有private
、protected
、readonly
修饰符,都会自动声明对应修饰符的实例属性。
class A {
constructor(
public a: number,
protected b: number,
private c: number,
readonly d: number
) {}
}
// 编译结果
class A {
a;
b;
c;
d;
constructor(a, b, c, d) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
}
}
6.静态成员
类的内部可以使用static
关键字,定义静态成员。
静态成员是只能通过类本身使用的成员,不能通过实例对象使用。
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x);
}
}
MyClass.x // 0
MyClass.printX() // 0
上面示例中,x
是静态属性,printX()
是静态方法。它们都必须通过MyClass
获取,而不能通过实例对象调用。
7.泛型类
类也可以写成泛型,使用类型参数。
class Box<Type> {
contents: Type;
constructor(value:Type) {
this.contents = value;
}
}
const b:Box<string> = new Box('hello!');
上面示例中,类Box
有类型参数Type
,因此属于泛型类。新建实例时,变量的类型声明需要带有类型参数的值,不过本例等号左边的Box<string>
可以省略不写,因为可以从等号右边推断得到。
注意,静态成员不能使用泛型的类型参数。
class Box<Type> {
static defaultContents: Type; // 报错
}
8.抽象类,抽象成员
TypeScript 允许在类的定义前面,加上关键字abstract
,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”(abstract class)。
abstract class A {
id = 1;
}
const a = new A(); // 报错
抽象类只能当作基类使用,用来在它的基础上定义子类。
abstract class A {
id = 1;
}
class B extends A {
amount = 100;
}
const b = new B();
b.id // 1
b.amount // 100
上面示例中,A
是一个抽象类,B
是A
的子类,继承了A
的所有成员,并且可以定义自己的成员和实例化。
9.this问题
类的方法经常用到this
关键字,它表示该方法当前所在的对象。
class A {
name = 'A';
getName() {
return this.name;
}
}
const a = new A();
a.getName() // 'A'
const b = {
name: 'b',
getName: a.getName
};
b.getName() // 'b'
上面示例中,变量a
和b
的getName()
是同一个方法,但是执行结果不一样,原因就是它们内部的this
指向不一样的对象。如果getName()
在变量a
上运行,this
指向a
;如果在b
上运行,this
指向b
。
十一、泛型
泛型的特点就是带有“类型参数”(type parameter)
有些时候,函数返回值的类型与参数类型是相关的。
function getFirst(arr) {
return arr[0];
}
上面示例中,函数getFirst()
总是返回参数数组的第一个成员。参数数组是什么类型,返回值就是什么类型。
这个函数的类型声明只能写成下面这样。
function f(arr:any[]):any {
return arr[0];
}
上面的类型声明,就反映不出参数与返回值之间的类型关系。
为了解决这个问题,TypeScript 就引入了“泛型”(generics)。泛型的特点就是带有“类型参数”(type parameter)。
1.泛型的写法
1.1函数的泛型写法
function
关键字定义的泛型函数,类型参数放在尖括号中,写在函数名后面。
function id<T>(arg:T):T {
return arg;
}
那么对于变量形式定义的函数,泛型有下面两种写法。
// 写法一
let myId:<T>(arg:T) => T = id;
// 写法二
let myId:{ <T>(arg:T): T } = id;
1.2接口的泛型写法
interface 也可以采用泛型的写法。
interface Box<Type> {
contents: Type;
}
let box:Box<string>;
泛型接口还有第二种写法。
interface Fn {
<Type>(arg:Type): Type;
}
function id<Type>(arg:Type): Type {
return arg;
}
let myId:Fn = id;
第二种写法还有一个差异之处。那就是它的类型参数定义在某个方法之中,其他属性和方法不能使用该类型参数。前面的第一种写法,类型参数定义在整个接口,接口内部的所有属性和方法都可以使用该类型参数。
1.3类的泛型写法
泛型类的类型参数写在类名后面。
class Pair<K, V> {
key: K;
value: V;
}
下面是继承泛型类的例子。
class A<T> {
value: T;
}
class B extends A<any> {
}
下面是另一个例子。
class C<NumType> {
value!: NumType;
add!: (x: NumType, y: NumType) => NumType;
}
let foo = new C<number>();
foo.value = 0;
foo.add = function (x, y) {
return x + y;
};
上面示例中,先新建类C
的实例foo
,然后再定义实例的value
属性和add()
方法。类的定义中,属性和方法后面的感叹号是非空断言,告诉 TypeScript 它们都是非空的,后面会赋值。
1.4 类型别名的泛型写法
type 命令定义的类型别名,也可以使用泛型。
type Nullable<T> = T | undefined | null;
上面示例中,Nullable<T>
是一个泛型,只要传入一个类型,就可以得到这个类型与undefined
和null
的一个联合类型。
下面是另一个例子。
type Container<T> = { value: T };
const a: Container<number> = { value: 0 };
const b: Container<string> = { value: 'b' };
下面是定义树形结构的例子。
type Tree<T> = {
value: T;
left: Tree<T> | null;
right: Tree<T> | null;
};
2.类型参数的默认值
类型参数可以设置默认值。使用时,如果没有给出类型参数的值,就会使用默认值。
function getFirst<T = string>(
arr:T[]
):T {
return arr[0];
}
上面示例中,T = string
表示类型参数的默认值是string
。调用getFirst()
时,如果不给出T
的值,TypeScript 就认为T
等于string
。
类型参数的默认值,往往用在类中。
class Generic<T = string> {
list:T[] = []
add(t:T) {
this.list.push(t)
}
}
上面示例中,类Generic
有一个类型参数T
,默认值为string
。这意味着,属性list
默认是一个字符串数组,方法add()
的默认参数是一个字符串。
3.数组的泛型表示
数组类型有一种表示方法是Array<T>
。这就是泛型的写法,Array
是 TypeScript 原生的一个类型接口,T
是它的类型参数。声明数组时,需要提供T
的值。
let arr:Array<number> = [1, 2, 3];
上面的示例中,Array<number>
就是一个泛型,类型参数的值是number
,表示该数组的全部成员都是数值。
同样的,如果数组成员都是字符串,那么类型就写成Array<string>
。事实上,在 TypeScript 内部,数组类型的另一种写法number[]
、string[]
,只是Array<number>
、Array<string>
的简写形式。
在 TypeScript 内部,Array
是一个泛型接口,类型定义基本是下面的样子。
interface Array<Type> {
length: number;
pop(): Type|undefined;
push(...items:Type[]): number;
// ...
}
4.类型参数的约束条件
TypeScript 提供了一种语法,允许在类型参数上面写明约束条件,如果不满足条件,编译时就会报错。这样也可以有良好的语义,对类型参数进行说明。
function comp<T extends { length: number }>(
a: T,
b: T
) {
if (a.length >= b.length) {
return a;
}
return b;
}
上面示例中,T extends { length: number }
就是约束条件,表示类型参数 T 必须满足{ length: number }
,否则就会报错。
十二、类型类型
TypeScript 提供了“类型断言”这样一种手段,允许开发者在代码中“断言”某个值的类型,告诉编译器此处的值是什么类型。TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。
这种做法的实质是,允许开发者在某个位置“绕过”编译器的类型推断,让本来通不过类型检查的代码能够通过,避免编译器报错。这样虽然削弱了 TypeScript 类型系统的严格性,但是为开发者带来了方便,毕竟开发者比编译器更了解自己的代码。
总之,类型断言并不是真的改变一个值的类型,而是提示编译器,应该如何处理这个值。
类型断言有两种语法。
// 语法一:<类型>值
<Type>value
// 语法二:值 as 类型
value as Type
上面两种语法是等价的,value
表示值,Type
表示类型。早期只有语法一,后来因为 TypeScript 开始支持 React 的 JSX 语法(尖括号表示 HTML 元素),为了避免两者冲突,就引入了语法二。目前,推荐使用语法二。
对象类型有严格字面量检查,如果存在额外的属性会报错。
// 报错
const p:{ x: number } = { x: 0, y: 0 };
上面示例中,等号右侧是一个对象字面量,多出了属性y
,导致报错。解决方法就是使用类型断言,可以用两种不同的断言。
// 正确
const p0:{ x: number } =
{ x: 0, y: 0 } as { x: number };
// 正确
const p1:{ x: number } =
{ x: 0, y: 0 } as { x: number; y: number };
上面示例中,两种类型断言都是正确的。第一种断言将类型改成与等号左边一致,第二种断言使得等号右边的类型是左边类型的子类型,子类型可以赋值给父类型,同时因为存在类型断言,就没有严格字面量检查了,所以不报错。
类型断言不应滥用,因为它改变了 TypeScript 的类型检查,很可能埋下错误的隐患。
const data:object = {
a: 1,
b: 2,
c: 3
};
data.length; // 报错
(data as Array<string>).length; // 正确
上面示例中,变量data
是一个对象,没有length
属性。但是通过类型断言,可以将它的类型断言为数组,这样使用length
属性就能通过类型检查。但是,编译后的代码在运行时依然会报错,所以类型断言可以让错误的代码通过编译
类型断言的一大用处是,指定 unknown 类型的变量的具体类型。
const value:unknown = 'Hello World';
const s1:string = value; // 报错
const s2:string = value as string; // 正确
上面示例中,unknown 类型的变量value
不能直接赋值给其他类型的变量,但是可以将它断言为其他类型,这样就可以赋值给别的变量了。
另外,类型断言也适合指定联合类型的值的具体类型。
const s1:number|string = 'hello';
const s2:number = s1 as number;
上面示例中,变量s1
是联合类型,可以断言其为联合类型里面的一种具体类型,再将其赋值给变量s2
。
1.类型断言条件
类型断言并不意味着,可以把某个值断言为任意类型。
const n = 1;
const m:string = n as string; // 报错
类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。
const n = 1;
const m:string = n as unknown as string; // 正确
上面示例中,通过两次类型断言,变量n
的类型就从数值,变成了完全无关的字符串,从而赋值时不会报错。
十三、namespace
1.基本用法
namespace 用来建立一个容器,内部的所有变量和函数,都必须在这个容器里面使用。
namespace Utils {
function isString(value:any) {
return typeof value === 'string';
}
// 正确
isString('yes');
}
Utils.isString('no'); // 报错
上面示例中,命名空间Utils
里面定义了一个函数isString()
,它只能在Utils
里面使用,如果用于外部就会报错。
如果要在命名空间以外使用内部成员,就必须为该成员加上export
前缀,表示对外输出该成员。
namespace Utility {
export function log(msg:string) {
console.log(msg);
}
export function error(msg:string) {
console.error(msg);
}
}
Utility.log('Call me');
Utility.error('maybe!');
上面示例中,只要加上export
前缀,就可以在命名空间外部使用内部成员。
编译出来的 JavaScript 代码如下。
var Utility;
(function (Utility) {
function log(msg) {
console.log(msg);
}
Utility.log = log;
function error(msg) {
console.error(msg);
}
Utility.error = error;
})(Utility || (Utility = {}));
2.namespace 的输出
namespace 本身也可以使用export
命令输出,供其他文件使用。
// shapes.ts
export namespace Shapes {
export class Triangle {
// ...
}
export class Square {
// ...
}
}
上面示例是一个文件shapes.ts
,里面使用export
命令,输出了一个命名空间Shapes
。
其他脚本文件使用import
命令,加载这个命名空间。
// 写法一
import { Shapes } from './shapes';
let t = new Shapes.Triangle();
// 写法二
import * as shapes from "./shapes";
let t = new shapes.Shapes.Triangle();
不过,更好的方法还是建议使用模块,采用模块的输出和输入。
// shapes.ts
export class Triangle {
/* ... */
}
export class Square {
/* ... */
}
// shapeConsumer.ts
import * as shapes from "./shapes";
let t = new shapes.Triangle();
上面示例中,使用模块的输出和输入,改写了前面的例子。
3.namespace的合并
多个同名的 namespace 会自动合并,这一点跟 interface 一样。
namespace Animals {
export class Cat {}
}
namespace Animals {
export interface Legged {
numberOfLegs: number;
}
export class Dog {}
}
// 等同于
namespace Animals {
export interface Legged {
numberOfLegs: number;
}
export class Cat {}
export class Dog {}
}
这样做的目的是,如果同名的命名空间分布在不同的文件中,TypeScript 最终会将它们合并在一起。这样就比较方便扩展别人的代码。
十四、装饰器
1.基本使用
装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为。
在语法上,装饰器有如下几个特征。
(1)第一个字符(或者说前缀)是@
,后面是一个表达式。
(2)@
后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。
(3)这个函数接受所修饰对象的一些相关值作为参数。
(4)这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。
举例来说,有一个函数Injectable()
当作装饰器使用,那么需要写成@Injectable
,然后放在某个类的前面。
@Injectable class A {
// ...
}
上面示例中,由于有了装饰器@Injectable
,类A
的行为在运行时就会发生改变。
下面就是一个最简单的装饰器。
function simpleDecorator() {
console.log('hi');
}
@simpleDecorator
class A {} // "hi"
装饰器有多种形式,基本上只要在@
符号后面添加表达式都是可以的。下面都是合法的装饰器。
@myFunc
@myFuncFactory(arg1, arg2)
@libraryModule.prop
@someObj.method(123)
@(wrap(dict['prop']))
注意,@
后面的表达式,最终执行后得到的应该是一个函数。
相比使用子类改变父类,装饰器更加简洁优雅,缺点是不那么直观,功能也受到一些限制。所以,装饰器一般只用来为类添加某种特定行为。
@frozen class Foo {
@configurable(false)
@enumerable(true)
method() {}
@throttle(500)
expensiveMethod() {}
}
上面示例中,一共有四个装饰器,一个用在类本身(@frozen
),另外三个用在类的方法(@configurable
、@enumerable
、@throttle
)。它们不仅增加了代码的可读性,清晰地表达了意图,而且提供一种方便的手段,增加或修改类的功能。
2.装饰器的结构
装饰器函数的类型定义如下。
type Decorator = (
value: DecoratedValue,
context: {
kind: string;
name: string | symbol;
addInitializer?(initializer: () => void): void;
static?: boolean;
private?: boolean;
access: {
get?(): unknown;
set?(value: unknown): void;
};
}
) => void | ReplacementValue;
上面代码中,Decorator
是装饰器的类型定义。它是一个函数,使用时会接收到value
和context
两个参数。
value
:所装饰的对象。context
:上下文对象,TypeScript 提供一个原生接口ClassMethodDecoratorContext
,描述这个对象。
function decorator(
value:any,
context:ClassMethodDecoratorContext
) {
// ...
}
上面是一个装饰器函数,其中第二个参数context
的类型就可以写成ClassMethodDecoratorContext
。
context
对象的属性,根据所装饰对象的不同而不同,其中只有两个属性(kind
和name
)是必有的,其他都是可选的。
(1)kind
:字符串,表示所装饰对象的类型,可能取以下的值。
- 'class'
- 'method'
- 'getter'
- 'setter'
- 'field'
- 'accessor'
这表示一共有六种类型的装饰器。
(2)name
:字符串或者 Symbol 值,所装饰对象的名字,比如类名、属性名等。
(3)addInitializer()
:函数,用来添加类的初始化逻辑。以前,这些逻辑通常放在构造函数里面,对方法进行初始化,现在改成以函数形式传入addInitializer()
方法。注意,addInitializer()
没有返回值。
(4)private
:布尔值,表示所装饰的对象是否为类的私有成员。
(5)static
:布尔值,表示所装饰的对象是否为类的静态成员。
(6)access
:一个对象,包含了某个值的 get 和 set 方法。
3.类装饰器
类装饰器的类型描述如下。
type ClassDecorator = (
value: Function,
context: {
kind: 'class';
name: string | undefined;
addInitializer(initializer: () => void): void;
}
) => Function | void;
类装饰器接受两个参数:value
(当前类本身)和context
(上下文对象)。其中,context
对象的kind
属性固定为字符串class
。
类装饰器一般用来对类进行操作,可以不返回任何值,请看下面的例子。
function Greeter(value, context) {
if (context.kind === 'class') {
value.prototype.greet = function () {
console.log('你好');
};
}
}
@Greeter
class User {}
let u = new User();
u.greet(); // "你好"
上面示例中,类装饰器@Greeter
在类User
的原型对象上,添加了一个greet()
方法,实例就可以直接使用该方法。
4.方法装饰器
方法装饰器用来装饰类的方法(method)。它的类型描述如下。
type ClassMethodDecorator = (
value: Function,
context: {
kind: 'method';
name: string | symbol;
static: boolean;
private: boolean;
access: { get: () => unknown };
addInitializer(initializer: () => void): void;
}
) => Function | void;
根据上面的类型,方法装饰器是一个函数,接受两个参数:value
和context
。
参数value
是方法本身,参数context
是上下文对象,有以下属性。
kind
:值固定为字符串method
,表示当前为方法装饰器。name
:所装饰的方法名,类型为字符串或 Symbol 值。static
:布尔值,表示是否为静态方法。该属性为只读属性。private
:布尔值,表示是否为私有方法。该属性为只读属性。access
:对象,包含了方法的存取器,但是只有get()
方法用来取值,没有set()
方法进行赋值。addInitializer()
:为方法增加初始化函数。
方法装饰器会改写类的原始方法,实质等同于下面的操作。
function trace(decoratedMethod) {
// ...
}
class C {
@trace
toString() {
return 'C';
}
}
// `@trace` 等同于
// C.prototype.toString = trace(C.prototype.toString);
上面示例中,@trace
是方法toString()
的装饰器,它的效果等同于最后一行对toString()
的改写。
如果方法装饰器返回一个新的函数,就会替代所装饰的原始函数。
function replaceMethod() {
return function () {
return `How are you, ${this.name}?`;
}
}
class Person {
constructor(name) {
this.name = name;
}
@replaceMethod
hello() {
return `Hi ${this.name}!`;
}
}
const robin = new Person('Robin');
robin.hello() // 'How are you, Robin?'
上面示例中,装饰器@replaceMethod
返回的函数,就成为了新的hello()
方法。
5.属性装饰器
属性装饰器用来装饰定义在类顶部的属性(field)。它的类型描述如下。
type 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;
注意,装饰器的第一个参数value
的类型是undefined
,这意味着这个参数实际上没用的,装饰器不能从value
获取所装饰属性的值。另外,第二个参数context
对象的kind
属性的值为字符串field
,而不是“property”或“attribute”,这一点是需要注意的。
属性装饰器要么不返回值,要么返回一个函数,该函数会自动执行,用来对所装饰属性进行初始化。该函数的参数是所装饰属性的初始值,该函数的返回值是该属性的最终值。
6.getter装饰器,setter装饰器
getter 装饰器和 setter 装饰器,是分别针对类的取值器(getter)和存值器(setter)的装饰器。它们的类型描述如下。
type ClassGetterDecorator = (
value: Function,
context: {
kind: 'getter';
name: string | symbol;
static: boolean;
private: boolean;
access: { get: () => unknown };
addInitializer(initializer: () => void): void;
}
) => Function | void;
type ClassSetterDecorator = (
value: Function,
context: {
kind: 'setter';
name: string | symbol;
static: boolean;
private: boolean;
access: { set: (value: unknown) => void };
addInitializer(initializer: () => void): void;
}
) => Function | void;
注意,getter 装饰器的上下文对象context
的access
属性,只包含get()
方法;setter 装饰器的access
属性,只包含set()
方法。
这两个装饰器要么不返回值,要么返回一个函数,取代原来的取值器或存值器。
下面的例子是将取值器的结果,保存为一个属性,加快后面的读取。
class C {
@lazy
get value() {
console.log('正在计算……');
return '开销大的计算结果';
}
}
function lazy(
value:any,
{kind, name}:any
) {
if (kind === 'getter') {
return function (this:any) {
const result = value.call(this);
Object.defineProperty(
this, name,
{
value: result,
writable: false,
}
);
return result;
};
}
return;
}
const inst = new C();
inst.value
// 正在计算……
// '开销大的计算结果'
inst.value
// '开销大的计算结果'
上面示例中,第一次读取inst.value
,会进行计算,然后装饰器@lazy
将结果存入只读属性value
,后面再读取这个属性,就不会进行计算了。
7.sccessor装饰器
装饰器语法引入了一个新的属性修饰符accessor
。
class C {
accessor x = 1;
}
上面示例中,accessor
修饰符等同于为属性x
自动生成取值器和存值器,它们作用于私有属性x
。也就是说,上面的代码等同于下面的代码。
class C {
#x = 1;
get x() {
return this.#x;
}
set x(val) {
this.#x = val;
}
}
accessor
也可以与静态属性和私有属性一起使用。
class C {
static accessor x = 1;
accessor #y = 2;
}
accessor 装饰器的类型如下。
type ClassAutoAccessorDecorator = (
value: {
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 装饰器的value
参数,是一个包含get()
方法和set()
方法的对象。该装饰器可以不返回值,或者返回一个新的对象,用来取代原来的get()
方法和set()
方法。此外,装饰器返回的对象还可以包括一个init()
方法,用来改变私有属性的初始值。
8.装饰器的执行顺序
装饰器的执行分为两个阶段。
(1)评估(evaluation):计算@
符号后面的表达式的值,得到的应该是函数。
(2)应用(application):将评估装饰器后得到的函数,应用于所装饰对象。
也就是说,装饰器的执行顺序是,先评估所有装饰器表达式的值,再将其应用于当前类。
应用装饰器时,顺序依次为方法装饰器和属性装饰器,然后是类装饰器。
请看下面的例子。
function 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;
}
@d('类装饰器')
class T {
@d('静态属性装饰器')
static staticField = log('静态属性值');
@d('原型方法')
[log('计算方法名')]() {}
@d('实例属性')
instanceField = log('实例属性值');
}
上面示例中,类T
有四种装饰器:类装饰器、静态属性装饰器、方法装饰器、属性装饰器。
它的运行结果如下。
// "评估 @d(): 类装饰器"
// "评估 @d(): 静态属性装饰器"
// "评估 @d(): 原型方法"
// "计算方法名"
// "评估 @d(): 实例属性"
// "应用 @d(): 原型方法"
// "应用 @d(): 静态属性装饰器"
// "应用 @d(): 实例属性"
// "应用 @d(): 类装饰器"
// "静态属性值"
可以看到,类载入的时候,代码按照以下顺序执行。
(1)装饰器评估:这一步计算装饰器的值,首先是类装饰器,然后是类内部的装饰器,按照它们出现的顺序。
注意,如果属性名或方法名是计算值(本例是“计算方法名”),则它们在对应的装饰器评估之后,也会进行自身的评估。
(2)装饰器应用:实际执行装饰器函数,将它们与对应的方法和属性进行结合。
原型方法的装饰器首先应用,然后是静态属性和静态方法装饰器,接下来是实例属性装饰器,最后是类装饰器。
注意,“实例属性值”在类初始化的阶段并不执行,直到类实例化时才会执行。
如果一个方法或属性有多个装饰器,则内层的装饰器先执行,外层的装饰器后执行。