TS使用教程

目录

一、基本用法

1.TS运行环境

2. ts编辑脚本代码

3. tsconfig.json配置文件 

 4.TS-node模块

二、 any 类型,unknown 类型,never 类型

1、any

1.1 基本含义

1.1.2 类型推断问题

1.1.3污染问题

2.unknown

3.never

三、类型系统

1.基本类型(继承js基本数据类型)

2.包装对象类型

3.Object 类型与 object 类型

3.1Oject类型

 3.2object类型

4.undefined和null的特殊性

5.联合类型

6.交叉类型

7.type命令

8.typeof运算符

9.块级类型声明

 10.类型的兼容

四、数组

1.类型推断

2.只读数组,const断言

3.多维数组

五、元组

 1.只读元组

2、成员数量推断

 3.扩展运算符与成员数量

六、symbol类型

1.unique symbol

 2.类型推断

七、函数类型

 1.Function类型

 2.箭头函数

3.可选参数

4.参数默认值

 5.参数解构

 6.rest参数

 7.readonly只读参数

 8.void类型

9.never类型

10.局部类型

11.函数重载

八、对象

1.可选属性

2.只读属性

3.属性名的索引类型 

4.解构赋值

5.结构类型原则

九、interface

1.interface对象的5种语法

 2.interface的继承

2.1.interface继承interface

2.2interface继承type

2.3interface继承class

3.接口合并

4.interface与type的异同

4.1interface与type的区别:

十、类

1.基本使用

1.1属性的类型

1.2.readonly修饰符

1.3 方法的类型

1.4.存取器的方法

1.5属性索引

2.类的interface接口

2.1implements关键字

2.2实现多个接口

 2.3类与接口的合并

 3.class类型

3.1实例类型

3.2类的自身类型

3.3结构类型原则

4.类的继承

5.可访问性修饰符

5.1public

5.2 private

5.3protected

5.4示例属性的简写形式

 6.静态成员

7.泛型类

 8.抽象类,抽象成员

9.this问题

十一、泛型

1.泛型的写法

1.1函数的泛型写法

 1.2接口的泛型写法

1.3类的泛型写法

1.4 类型别名的泛型写法

 2.类型参数的默认值

3.数组的泛型表示

 4.类型参数的约束条件

十二、类型类型

1.类型断言条件

十三、namespace

1.基本用法

2.namespace 的输出

3.namespace的合并

十四、装饰器

1.基本使用

2.装饰器的结构

3.类装饰器

4.方法装饰器

5.属性装饰器

6.getter装饰器,setter装饰器

7.sccessor装饰器

8.装饰器的执行顺序


一、基本用法

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 有两个“顶层类型”(anyunknown),但是“底层类型”只有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()的参数变量xy,都没有足够的信息,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.上面所有类型的名称都是小写字母,首字母大写的NumberStringBoolean等在 JavaScript 语言中都是内置对象,而不是类型名称

2.如果没有声明类型的变量,被赋值为undefinednull,它们的类型会被推断为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;

除了undefinednull这两个值不能转为对象,其他任何值都可以赋值给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的特殊性

undefinednull既是值,又是类型

作为值,它们有一个特殊的地方:任何其他类型的变量都可以赋值为undefinednull

JavaScript 的行为是,变量如果等于undefined就表示还没有赋值,如果等于null就表示值为空。所以,TypeScript 就允许了任何类型的变量都可以赋值为这两个值。

let age:number = 24;

age = null;      // 报错
age = undefined; // 报错

上面示例中,打开--strictNullChecks以后,number类型的变量age就不能赋值为undefinednull

这个选项在配置文件tsconfig.json的写法如下。

{
  "compilerOptions": {
    "strictNullChecks": true
    // ...
  }
}

打开strictNullChecks以后,undefinednull这两种值也不能互相赋值了。undefinednull只能赋值给自身,或者any类型和unknown类型的变量

let x:any     = undefined;
let y:unknown = null;

5.联合类型

联合类型(union types)指的是多个类型组成的一个新类型,使用符号|表示。

联合类型A|B表示,任何一个类型只要属于AB,就属于联合类型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;

 变量ab的类型是不一样的,但是变量a赋值给变量b并不会报错。这时,我们就认为,b的类型兼容a的类型。

如果类型A的值可以赋值给类型B,那么类型A就称为类型B的子类型(subtype)。在上例中,类型number就是类型number|string的子类型

let a:'hi' = 'hi';
let b:string = 'hello';

b = a; // 正确
a = b; // 报错

上面示例中,histring的子类型,stringhi的父类型。所以,变量a可以赋值给变量b,但是反过来就会报错。

子类型继承了父类型的所有特征,所以可以用在父类型的场合。但是,子类型还可能有一些父类型没有的特征,所以父类型不能用在子类型的场合。

四、数组

TypeScript 数组有一个根本特征:所有成员的类型必须相同,但是成员数量是不确定的,可以是无限数量的成员,也可以是零成员

声明的方式:

let arr:number[] = [1, 2, 3];
let arr:(number|string)[];
let arr:Array = [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

上面示例中,变量xy的类型都是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 // 报错

变量ab是两个类型,就不能把一个赋值给另一个。

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;
}

上面示例中,xy都是对象的属性,分别使用冒号指定每个属性的类型。

(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其实有两个属性nameradius。这时,Circle是子接口,Shape是父接口。

extends关键字会从继承的接口里面拷贝属性类型,这样就不必书写重复的属性。

interface 允许多重继承。

interface Style {
  color: string;
}

interface Shape {
  name: string;
}

interface Circle extends Style, Shape {
  radius: number;
}

上面示例中,Circle同时继承了StyleShape,所以拥有三个属性colornameradius

多重接口继承,实际上相当于多个父接口的合并

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就具有属性xy()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接口会合并成一个接口,同时有heightwidthlength三个属性。

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
}

上面示例中,类型BearAnimal的基础上添加了一个属性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 = '';
}

上面示例中,interfacetype都可以定义一个对象类型。类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()的第一个参数PointClassPoint类自身,要声明这个参数的类型,简便的方法就是使用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('');

上面示例中,PointPosition的静态属性和构造方法都不一样,但因为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();

上面示例中,AB都有私有成员(或保护成员)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)控制:publicprivateprotected

这三个修饰符的位置,都写在属性或方法的最前面

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;
  }
}

上面实例中,属性xy的值是通过构造方法的参数传入的。

这样的写法等于对同一个属性要声明两次类型,一次在类的头部,另一次在构造方法的参数里面。这有些累赘,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修饰符,构造方法的参数名只要有privateprotectedreadonly修饰符,都会自动声明对应修饰符的实例属性。

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 {
  contents: Type;

  constructor(value:Type) {
    this.contents = value;
  }
}

const b:Box = new Box('hello!');

上面示例中,类Box有类型参数Type,因此属于泛型类。新建实例时,变量的类型声明需要带有类型参数的值,不过本例等号左边的Box可以省略不写,因为可以从等号右边推断得到。

注意,静态成员不能使用泛型的类型参数。

class Box {
  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是一个抽象类,BA的子类,继承了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'

上面示例中,变量abgetName()是同一个方法,但是执行结果不一样,原因就是它们内部的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(arg:T):T {
  return arg;
}

那么对于变量形式定义的函数,泛型有下面两种写法。

// 写法一
let myId:(arg:T) => T = id;

// 写法二
let myId:{ (arg:T): T } = id;

 1.2接口的泛型写法

interface 也可以采用泛型的写法。

interface Box {
  contents: Type;
}

let box:Box;

泛型接口还有第二种写法。

interface Fn {
  (arg:Type): Type;
}

function id(arg:Type): Type {
  return arg;
}

let myId:Fn = id;

 第二种写法还有一个差异之处。那就是它的类型参数定义在某个方法之中,其他属性和方法不能使用该类型参数。前面的第一种写法,类型参数定义在整个接口,接口内部的所有属性和方法都可以使用该类型参数。

1.3类的泛型写法

泛型类的类型参数写在类名后面。

class Pair {
  key: K;
  value: V;
}

下面是继承泛型类的例子。

class A {
  value: T;
}

class B extends A {
}

下面是另一个例子。

class C {
  value!: NumType;
  add!: (x: NumType, y: NumType) => NumType;
}

let foo = new C();

foo.value = 0;
foo.add = function (x, y) {
  return x + y;
};

上面示例中,先新建类C的实例foo,然后再定义实例的value属性和add()方法。类的定义中,属性和方法后面的感叹号是非空断言,告诉 TypeScript 它们都是非空的,后面会赋值。

1.4 类型别名的泛型写法

type 命令定义的类型别名,也可以使用泛型。

type Nullable = T | undefined | null;

上面示例中,Nullable是一个泛型,只要传入一个类型,就可以得到这个类型与undefinednull的一个联合类型。

下面是另一个例子。

type Container = { value: T };

const a: Container = { value: 0 };
const b: Container = { value: 'b' };

下面是定义树形结构的例子。

type Tree = {
  value: T;
  left: Tree | null;
  right: Tree | null;
};

 2.类型参数的默认值

类型参数可以设置默认值。使用时,如果没有给出类型参数的值,就会使用默认值。

function getFirst(
  arr:T[]
):T {
  return arr[0];
}

上面示例中,T = string表示类型参数的默认值是string。调用getFirst()时,如果不给出T的值,TypeScript 就认为T等于string

类型参数的默认值,往往用在类中。

class Generic {
  list:T[] = []

  add(t:T) {
    this.list.push(t)
  }
}

上面示例中,类Generic有一个类型参数T,默认值为string。这意味着,属性list默认是一个字符串数组,方法add()的默认参数是一个字符串。

3.数组的泛型表示

数组类型有一种表示方法是Array。这就是泛型的写法,Array是 TypeScript 原生的一个类型接口,T是它的类型参数。声明数组时,需要提供T的值。

let arr:Array = [1, 2, 3];

上面的示例中,Array就是一个泛型,类型参数的值是number,表示该数组的全部成员都是数值。

同样的,如果数组成员都是字符串,那么类型就写成Array。事实上,在 TypeScript 内部,数组类型的另一种写法number[]string[],只是ArrayArray的简写形式。

在 TypeScript 内部,Array是一个泛型接口,类型定义基本是下面的样子。

interface Array {

  length: number;

  pop(): Type|undefined;

  push(...items:Type[]): number;

  // ...
}

 4.类型参数的约束条件

TypeScript 提供了一种语法,允许在类型参数上面写明约束条件,如果不满足条件,编译时就会报错。这样也可以有良好的语义,对类型参数进行说明。

function comp(
  a: T,
  b: T
) {
  if (a.length >= b.length) {
    return a;
  }
  return b;
}

上面示例中,T extends { length: number }就是约束条件,表示类型参数 T 必须满足{ length: number },否则就会报错。

十二、类型类型

TypeScript 提供了“类型断言”这样一种手段,允许开发者在代码中“断言”某个值的类型,告诉编译器此处的值是什么类型。TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。

这种做法的实质是,允许开发者在某个位置“绕过”编译器的类型推断,让本来通不过类型检查的代码能够通过,避免编译器报错。这样虽然削弱了 TypeScript 类型系统的严格性,但是为开发者带来了方便,毕竟开发者比编译器更了解自己的代码。

总之,类型断言并不是真的改变一个值的类型,而是提示编译器,应该如何处理这个值

类型断言有两种语法。

// 语法一:<类型>值
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).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是装饰器的类型定义。它是一个函数,使用时会接收到valuecontext两个参数。

  • value:所装饰的对象。
  • context:上下文对象,TypeScript 提供一个原生接口ClassMethodDecoratorContext,描述这个对象。
function decorator(
  value:any,
  context:ClassMethodDecoratorContext
) {
  // ...
}

上面是一个装饰器函数,其中第二个参数context的类型就可以写成ClassMethodDecoratorContext

context对象的属性,根据所装饰对象的不同而不同,其中只有两个属性(kindname)是必有的,其他都是可选的。

(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;

根据上面的类型,方法装饰器是一个函数,接受两个参数:valuecontext

参数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 装饰器的上下文对象contextaccess属性,只包含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)装饰器应用:实际执行装饰器函数,将它们与对应的方法和属性进行结合。

原型方法的装饰器首先应用,然后是静态属性和静态方法装饰器,接下来是实例属性装饰器,最后是类装饰器。

注意,“实例属性值”在类初始化的阶段并不执行,直到类实例化时才会执行。

如果一个方法或属性有多个装饰器,则内层的装饰器先执行,外层的装饰器后执行。

你可能感兴趣的:(前端,typescript)