跳至主要內容

阮一峰 TypeScript 教程 笔记

Scarb大约 27 分钟

原文地址:http://hscarb.github.io/other/99991231-ryf-ts-note.htmlopen in new window

阮一峰 TypeScript 教程 笔记

https://wangdoc.com/typescript/open in new window

1. 简介

1.3 动态类型与静态类型

TypeScript 引入了一个更强大、更严格的类型系统,属于静态类型语言。变量类型和属性都是静态的,不允许随机增删

2. 基本用法

2.3 TypeScript 的编译

TypeScript 官方没有做运行环境,只提供编译器。编译时,会将类型声明和类型相关的代码全部删除,只留下能运行的 JavaScript 代码,并且不会改变 JavaScript 的运行结果。

2.6 tsc 编译器

npm install -g typescript
tsc -v
tsc -h
tsc --all
pnpm add typescript -D
pnpm tsc
tsc file1.ts file2.ts --outFile app.js	# 多个文件编译成一个
tsc app.ts --outDir dist	# 指定保存到其他目录
tsc --target es2015 app.ts	# 指定编译后的 JavaScript 版本
tsc --noEmitOnError app.ts	# 报错就停止编译,不生成编译产物
tsc --noEmit app.ts			# 只检查类型是否正确,不生成 JavaScript 文件

2.6.5 tsconfig.json

$ tsc file1.ts file2.ts --outFile dist/app.js

上面这个命令写成tsconfig.json,就是下面这样。

{
  "files": ["file1.ts", "file2.ts"],
  "compilerOptions": {
    "outFile": "dist/app.js"
  }
}

有了这个配置文件,编译时直接调用tsc命令就可以了。

$ tsc

2.7 ts-node 模块

ts-nodeopen in new window 是一个非官方的 npm 模块,可以直接运行 TypeScript 代码。

$ npm install -g ts-node
$ ts-node script.ts
$ ts-node
> const twice = (x:string) => x + x;
> twice('abc')
'abcabc'
> 

3. any 类型,unknown 类型,never 类型

TypeScript 有两个“顶层类型”(anyunknown),但是“底层类型”只有never唯一一个。

3.1 any 类型

没有任何限制,该类型的变量可以赋予任意类型的值。

对于开发者没有指定类型、TypeScript 必须自己推断类型的那些变量,如果无法推断出类型,TypeScript 就会认为该变量的类型是any

any类型除了关闭类型检查,还会“污染”其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错。

let x:any = 'hello';
let y:number;

y = x; // 不报错

y * 123 // 不报错
y.toFixed() // 不报错

3.2 unknown 类型

any 含义相同,表示类型不确定。解决 any 类型污染问题。

限制:

  1. 不能直接赋值给其他类型的变量(除了any类型和unknown类型)。

    let v:unknown = 123;
    
    let v1:boolean = v; // 报错
    let v2:number = v; // 报错
    
  2. 不能直接调用 unknown 类型变量的方法和属性。

  3. 能够进行的运算是有限的,只能进行比较运算(运算符=====!=!==||&&?)、取反运算(运算符!)、typeof运算符和instanceof运算符这几种,其他运算都会报错。

  • 只有经过类型缩小(缩小 unknown 变量类型范围)才能使用

    let a:unknown = 1;
    
    if (typeof a === 'number') {
      let r = a + 10; // 正确
    }
    
    let s:unknown = 'hello';
    
    if (typeof s === 'string') {
      s.length; // 正确
    }
    

3.3 never 类型

空类型,不包含任何值。不能给它赋任何值。

使用场景:

  • 在一些类型运算中,保证类型运算的完整性。

    function fn(x:string|number) {
      if (typeof x === 'string') {
        // ...
      } else if (typeof x === 'number') {
        // ...
      } else {
        x; // never 类型
      }
    }
    

* 可以赋值给任意其他类型

  ```ts
  function f():never {
    throw new Error('Error');
  }
  
  let v1:number = f(); // 不报错
  let v2:string = f(); // 不报错
  let v3:boolean = f(); // 不报错

4. 类型系统

4.1 基本类型

JavaScript 语言(注意,不是 TypeScript)将值分成8种类型。

  • boolean
  • string
  • number
  • bigint
  • symbol
  • object:对象、数组、函数
  • undefined:未定义
  • null:空

TypeScript 继承了 JavaScript 的类型设计,以上8种类型可以看作 TypeScript 的基本类型。

注意,上面所有类型的名称都是小写字母,首字母大写的NumberStringBoolean等在 JavaScript 语言中都是内置对象,而不是类型名称。

另外,undefined 和 null 既可以作为值,也可以作为类型,取决于在哪里使用它们。

4.2 包装对象类型

4.2.1 包装对象的概念

JavaScript 的8种类型之中,五种属于原始类型(primitive value),代表最基本的、不可再分的值。

  • boolean
  • string
  • number
  • bigint
  • symbol

五种原始类型的值,都有对应的包装对象(wrapper object)。指的是这些值在需要时,会自动产生的对象。

Symbol()BigInt()不能作为构造函数使用,剩下三种可以。

  • Boolean()
  • String()
  • Number()
const s = new String('hello');
typeof s // 'object'
s.charAt(1) // 'e'

4.2.2 包装对象类型与字面量类型

String类型可以赋值为字符串的字面量,也可以赋值为包装对象。但是,string类型只能赋值为字面量,赋值为包装对象就会报错。

const s1:String = 'hello'; // 正确
const s2:String = new String('hello'); // 正确

const s3:string = 'hello'; // 正确
const s4:string = new String('hello'); // 报错

建议只使用小写类型,不使用大写类型。因为绝大部分使用原始类型的场合,都是使用字面量,不使用包装对象。而且,TypeScript 把很多内置方法的参数,定义成小写类型,使用大写类型会报错。

4.3 Object 类型与 object 类型

4.3.1 Object 类型

所有可以转成对象的值,都是Object类型,这囊括了几乎所有的值。除了undefinednull这两个值不能转为对象,其他任何值都可以赋值给Object类型。

{}Object类型的简写形式,所以使用Object时常常用空对象代替。

let obj:{};
 
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;

4.3.2 object 类型

小写的object类型代表 JavaScript 里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数,不包括原始类型的值。

let obj:object;
 
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
obj = true; // 报错
obj = 'hi'; // 报错
obj = 1; // 报错

大多数时候,我们使用对象类型,只希望包含真正的对象,不希望包含原始类型。所以,建议总是使用小写类型object,不使用大写类型Object

4.4 undefined 和 null 的特殊性

任何其他类型的变量都可以赋值为undefinednull

4.5 值类型

单个值也是一种类型,称为“值类型”。

遇到const命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。

let x:'hello';

x = 'hello'; // 正确
x = 'world'; // 报错

// x 的类型是 "https"
const x = 'https';

// y 的类型是 string
const y:string = 'https';

只包含单个值的值类型,用处不大。实际开发中,往往将多个值结合,作为联合类型使用。

4.6 联合类型

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

联合类型A|B表示,任何一个类型只要属于AB,就属于联合类型A|B

let x:string|number;

x = 123; // 正确
x = 'abc'; // 正确

联合类型可以与值类型相结合,表示一个变量的值有若干种可能。

let setting:true|false;
let gender:'male'|'female';
let rainbowColor:'赤'|'橙'|'黄'|'绿'|'青'|'蓝'|'紫';
let name:string|null;
let x:
  | 'one'
  | 'two'
  | 'three'
  | 'four';

如果一个变量有多种类型,读取该变量时,往往需要进行“类型缩小”(type narrowing),区分该值到底属于哪一种类型,然后再进一步处理。

function getPort(scheme: 'http'|'https') {
  switch (scheme) {
    case 'http':
      return 80;
    case 'https':
      return 443;
  }
}

4.7 交叉类型

交叉类型A&B表示,任何一个类型必须同时属于AB,才属于交叉类型A&B

// 主要用途是表示对象的合成
let obj:
  { foo: string } &
  { bar: string };

obj = {
  foo: 'hello',
  bar: 'world'
};

// 为对象类型添加新属性
type A = { foo: number };

type B = A & { bar: number };

4.8 type 命令

type命令用来定义一个类型的别名。作用域是块级作用域

type Age = number;

let age:Age = 55;

4.9 typeof 运算符

JavaScript 里面,typeof运算符只可能返回八种结果,而且都是字符串。

typeof undefined; // "undefined"
typeof true; // "boolean"
typeof 1337; // "number"
typeof "foo"; // "string"
typeof {}; // "object"
typeof parseInt; // "function"
typeof Symbol(); // "symbol"
typeof 127n // "bigint"

TypeScript 将typeof运算符移植到了类型运算,它的操作数依然是一个值,但是返回的不是字符串,而是该值的 TypeScript 类型。

const a = { x: 0 };

type T0 = typeof a;   // { x: number }
type T1 = typeof a.x; // number

let a = 1;
let b:typeof a;

if (typeof a === 'number') {
  b = a;
}

typeof命令的参数不能是类型。

4.10 块级类型声明

类型可以声明在代码块(用大括号表示)里面,并且只在当前代码块有效。

if (true) {
  type T = number;
  let v:T = 5;
} else {
  type T = string;
  let v:T = 'hello';
}

4.11 类型的兼容

type T = number|string;

let a:number = 1;
let b:T = a;

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

凡是可以使用父类型的地方,都可以使用子类型,但是反过来不行。

5. 数组

JavaScript 数组在 TypeScript 里面分成两种类型,分别是数组(array)和元组(tuple)。

5.1 简介

TypeScript 的数组所有成员类型必须相同。

let arr:number[] = [1, 2, 3];
let arr:(number|string)[];
let arr:any[];

// 另一种写法,用 Array 接口
let arr:Array<number> = [1, 2, 3];
let arr:Array<number|string>;

5.2 数组的类型推断

// 推断为 any[]
const arr = [];
// 赋值时会自动更新类型推断
arr.push(123);
arr // 推断类型为 number[]

arr.push('abc');
arr // 推断类型为 (string|number)[]

类型推断的自动更新只发生初始值为空数组的情况。如果初始值不是空数组,类型推断就不会更新。

5.3 只读数组,const 断言

JavaScript const命令声明的数组变量是可以改变成员。TypeScript 允许声明只读数组,方法是在数组类型前面加上readonly关键字。

TypeScript 将readonly number[]number[]视为两种不一样的类型,后者是前者的子类型。(数组是只读数组的子类型)

const arr:readonly number[] = [0, 1];

arr[1] = 2; // 报错
arr.push(3); // 报错
delete arr[0]; // 报错

// 另外写法
const a1:ReadonlyArray<number> = [0, 1];
const a2:Readonly<number[]> = [0, 1];

5.4 多维数组

var multi:number[][] = [[1,2,3], [23,24,25]];

6. 元组

6.1 简介

TypeScript 特有的数据类型,各个成员的类型可以不同的数组。必须声明每个成员的类型。

// 数组的成员类型写在方括号外面(number[]),元组的成员类型是写在方括号里面([number])
const s:[string, string, boolean] = ['a', 'b', true];
// 问号后缀表示该成员是可选的,可选成员必须在必选成员之后
let a:[number, number?] = [1];
// 扩展运算符 ... 表示不限成员数量的元组,它可以用在任意位置
type NamedNums = [
  string,
  ...number[]
];
const a:NamedNums = ['A', 1, 2];
// 不确定元组成员类型和数量,可以放置任意数量和类型的成员
type Tuple = [...any[]];
// 方括号读取成员类型
type Tuple = [string, number];
type Age = Tuple[1]; // number

6.2 只读元组

type t = readonly [number, string]
type t = Readonly<[number, string]>		// 泛型写法Readonly<T>

6.4 扩展运算符

扩展运算符(...)将数组(注意,不是元组)转换成一个逗号分隔的序列,这时 TypeScript 会认为这个序列的成员数量是不确定的,因为数组的成员数量是不确定的。

const arr = [1, 2, 3];
console.log(...arr)

元组使用扩展运算符,成员数量是确定的。

7. symbol 类型

7.1 简介

Symbol 是 ES2015 新引入的一种原始类型的值。它类似于字符串,但是每一个 Symbol 值都是独一无二的,与其他任何值都不相等。

let x:symbol = Symbol();
let y:symbol = Symbol();

x === y // false

8. 函数

8.1 简介

需要在声明函数时,给出参数的类型和返回值的类型。缺乏足够信息,就会推断该参数的类型为any

// 写法一
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 允许省略参数。

8.2 Function 类型

Function 类型表示函数

8.3 箭头函数

普通函数的一种简化写法。

const repeat = (str:string, times:number):string => str.repeat(times);

function greet(fn:(a:string) => void):void {
  fn('world');
}

8.4 可选参数

function f(x?:number) {
  // ...
}

f(); // OK
f(10); // OK

8.5 参数默认值

function createPoint(x:number = 0, y:number = 0):[number, number] {
  return [x, y];
}

createPoint() // [0, 0]

8.6 参数解构

可以用类型别名

type ABC = { a:number; b:number; c:number };

function sum({ a, b, c }:ABC) {
  console.log(a + b + c);
}

8.7 rest 参数

表示函数剩余的所有参数,可以试数组,也可以是元组。

// rest 参数为数组
function joinNumbers(...nums:number[]) {
  // ...
}

// rest 参数为元组
function f(...args:[boolean, number]) {
  // ...
}

8.8 readonly 只读参数

function arraySum(arr:readonly number[]) {
  // ...
  arr[0] = 0; // 报错
}

8.9 void 类型

表示函数没有返回值

function f():void {
  console.log('hello');
}

8.10 never 类型

never类型表示肯定不会出现的值。它用在函数的返回值,就表示某个函数肯定不会返回值,即函数不会正常执行结束。

抛出错误的函数

function fail(msg:string):never {
  throw new Error(msg);
}

无限执行的函数

const sing = function():never {
  while (true) {
    console.log('sing');
  }
};

8.11 局部类型

声明其他类型,只在函数内部有效

function hello(txt:string) {
  type message = string;
  let newTxt:message = 'hello ' + txt;
  return newTxt;
}

const newTxt:message = hello('world'); // 报错

8.12 高阶函数

函数的返回值还是一个函数,那么前一个函数就称为高阶函数(higher-order function)。

(someValue: number) => (multiplier: number) => someValue * multiplier;

8.13 函数重载

接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。

TypeScript 对于“函数重载”的类型声明方法是,逐一定义每一种情况的类型。

// 声明
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();
}

8.14 构造函数

使用new命令调用。构造函数的类型写法,就是在参数列表前面加上new命令。

class Animal {
  numLegs:number = 4;
}
// 构造函数
type AnimalConstructor = new () => Animal;
// 传入一个构造函数
function create(c:AnimalConstructor):Animal {
  return new c();
}

const a = create(Animal);

9. 对象

9.1 简介

const obj:{
  x:number;		// 可以以分号结尾
  y:number;
  add(x:number, y:number): number;
} = { x: 1, y: 1 };

// 属性类型以逗号结尾
type MyObj = {
  x:number,
  y:number,
};

9.2 可选属性

在属性名后面加一个问号。

const obj: {
  x: number;
  y?: number;
} = { x: 1 };

// 可选属性读取之前,需要判断是否为undefined才能使用
// 写法一
let firstName = (user.firstName === undefined) ? 'Foo' : user.firstName;
let lastName = (user.lastName === undefined) ? 'Bar' : user.lastName;

// 写法二,使用Null判断运算符??
let firstName = user.firstName ?? 'Foo';
let lastName = user.lastName ?? 'Bar';

9.3 只读属性

const person:{
  readonly age: number
} = { age: 20 };

person.age = 21; // 报错

// 只能在对象初始化时赋值
type Point = {
  readonly x: number;
  readonly y: number;
};

const p:Point = { x: 0, y: 0 };

p.x = 100; // 报错

9.4 属性名的索引类型

type MyObj = {
  [property: string]: string	// 不管这个对象有多少属性,只要属性名为字符串,且属性值也是字符串,就符合这个类型声明。
};

const obj:MyObj = {
  foo: 'a',
  bar: 'b',
  baz: 'c',
};

9.5 解构赋值

用于直接从对象中提取属性

const {id, name, price} = product;
// 另一种写法:类型写法
const {id, name, price}:{
  id: string;
  name: string;
  price: number
} = product;

9.7 严格字面量检查

const point:{
  x:number;
  y:number;
} = {
  x: 1,
  y: 1,
  z: 1 // 报错
};

const myPoint = {
  x: 1,
  y: 1,
  z: 1
};

const point:{
  x:number;
  y:number;
} = myPoint; // 正确,等号右边是变量,不触发严格字面量检查

9.9 空对象

这种写法其实在 JavaScript 很常见:先声明一个空对象,然后向空对象添加属性。但是,TypeScript 不允许动态添加属性,所以对象不能分步生成,必须生成时一次性声明所有属性。

// 错误
const pt = {};
pt.x = 3;
pt.y = 4;

// 正确
const pt = {
  x: 3,
  y: 4
};

10. interface

10.1 简介

对象的模板,使用了某个模板的对象,就拥有了指定的类型结构。

它的成员有5种形式。

  • 对象属性
  • 对象的属性索引
  • 对象方法
  • 函数
  • 构造函数
interface Point {
  x: number;
  y: number;
  s?: string;
  readonly a: string;
  // 属性索引
  [prop: string]: number;
  // 对象方法
  f(x: boolean): string;
  f: (x: boolean) => string;
  f: { (x: boolean): string };
  // 函数
  (x:number, y:number): number;
  // 构造函数
  new (message?: string): Error;
}

interface 里面的函数重载,不需要给出实现。但是,由于对象内部定义方法时,无法使用函数重载的语法,所以需要额外在对象外部给出函数方法的实现

interface A {
  f(): number;
  f(x: boolean): boolean;
  f(x: string, y: string): string;
}
// 额外在对象外部给出函数方法的实现
function MyFunc(): number;
function MyFunc(x: boolean): boolean;
function MyFunc(x: string, y: string): string;
function MyFunc(
  x?:boolean|string, y?:string
):number|boolean|string {
  if (x === undefined && y === undefined) return 1;
  if (typeof x === 'boolean' && y === undefined) return true;
  if (typeof x === 'string' && typeof y === 'string') return 'hello';
  throw new Error('wrong parameters');  
}

const a:A = {
  f: MyFunc
}

10.2 interface 的继承

10.2.1 interface 继承 interface

interface Shape {
  name: string;
}

interface Circle extends Shape {
  radius: number;
}

允许继承多个,子接口与父接口的同名属性必须是类型兼容的,不能有冲突,否则会报错。

10.2.2 interface 继承 type

type Country = {
  name: string;
  capital: string;
}

interface CountryWithPop extends Country {
  population: number;
}

10.2.3 interface 继承 class

class A {
  x:string = '';

  y():boolean {
    return true;
  }
}

interface B extends A {
  z: number
}

10.3 接口合并

多个同名接口会合并成一个接口。

interface Box {
  height: number;
  width: number;
}

interface Box {
  length: number;
}

函数重载。而且,后面的定义比前面的定义具有更高的优先级。但字面量类型有更高的优先级。

interface Cloner {
  clone(animal: Animal): Animal;
}

interface Cloner {
  clone(animal: Sheep): Sheep;
}

interface Cloner {
  clone(animal: Dog): Dog;
  clone(animal: Cat): Cat;
}

// 等同于
interface Cloner {
  clone(animal: Dog): Dog;
  clone(animal: Cat): Cat;
  clone(animal: Sheep): Sheep;
  clone(animal: Animal): Animal;
}

10.4 interface 与 type 的异同

几乎所有的 interface 命令都可以改写为 type 命令,他们可以定义一个类型,class命令也有类似作用,通过定义一个类,同时定义一个对象类型。但是,它会创造一个值,编译后依然存在。如果只是单纯想要一个类型,应该使用typeinterface

type Country = {
  name: string;
  capital: string;
}

interface Coutry {
  name: string;
  capital: string;
}

区别

  1. type能够表示非对象类型,而interface只能表示对象类型(包括数组、函数等)。

  2. interface可以继承其他类型,type不支持继承。

  3. 同名interface会自动合并,同名type则会报错。也就是说,TypeScript 不允许使用type多次定义同一个类型。

  4. interface不能包含属性映射(mapping),type可以。

  5. this关键字只能用于interface

  6. type 可以扩展原始数据类型,interface 不行。

  7. interface无法表达某些复杂类型(比如交叉类型和联合类型),但是type可以。

综上所述,如果有复杂的类型运算,那么没有其他选择只能使用type;一般情况下,interface灵活性比较高,便于扩充类型或自动合并,建议优先使用。

11. 类

11.1 简介

class Point {
  x:number;
  y:number;

  // 函数重载
  constructor(x:number, y:string);
  constructor(s:string);
  constructor(xs:number|string, y?:string) {
    // ...
  }

  add(point:Point) {
    return new Point(
      this.x + point.x,
      this.y + point.y
    );
  }
}

11.1.4 存取器方法

存取器(accessor)是特殊的类方法,包括取值器(getter)和存值器(setter)两种方法。

它们用于读写某个属性,取值器用来读取属性,存值器用来写入属性。

class C {
  _name = '';
  get name() {
    return this._name;
  }
  set name(value) {
    this._name = value;
  }
}

11.1.5 属性索引

class MyClass {
  [s:string]: boolean |
    ((s:string) => boolean);

  get(s:string) {
    return this[s] as boolean;
  }
}

[s:string]表示所有属性名类型为字符串的属性,它们的属性值要么是布尔值,要么是返回布尔值的函数

11.2 类的 interface 接口

11.2.1 implements 关键字

interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。

interface 只是指定检查条件,如果不满足这些条件就会报错。它并不能代替 class 自身的类型声明。

interface Country {
  name:string;
  capital:string;
}
// 或者
type Country = {
  name:string;
  capital:string;
}

class MyCountry implements Country {
  name = '';
  capital = '';
}

11.2.2 实现多个接口

class Car implements MotorVehicle {
}

class SecretCar extends Car implements Flyable, Swimmable {
}

11.3 Class 类型

11.3.1 实例类型

interface MotorVehicle {
}

class Car implements MotorVehicle {
}

// 写法一
const c1:Car = new Car();
// 写法二
const c2:MotorVehicle = new Car();

变量的类型可以写成类Car,也可以写成接口MotorVehicle。它们的区别是,如果类Car有接口MotoVehicle没有的属性和方法,那么只有变量c1可以调用这些属性和方法。

11.3.2 类的自身类型

用 typeof 运算符可以获得类的自身类型。

11.5 可访问性修饰符

类的内部成员的外部可访问性,由三个可访问性修饰符(access modifiers)控制:publicprivateprotected

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

public修饰符是默认修饰符,如果省略不写,实际上就带有该修饰符。因此,类的属性和方法默认都是外部可访问的。

class Greeter {
  public greet() {
    console.log("hi!");
  }
}

const g = new Greeter();
g.greet();

11.5.4 实例属性的简写形式

构造方法的参数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;
    }
}

11.6 静态成员

静态成员是只能通过类本身使用的成员,不能通过实例对象使用。类的内部可以使用static关键字,定义静态成员。static关键字前面可以使用 public、private、protected 修饰符。

class MyClass {
  static x = 0;
  static printX() {
    console.log(MyClass.x);
  }
}

MyClass.x // 0
MyClass.printX() // 0

11.7 泛型类

class Box<Type> {
  contents: Type;

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

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

新建实例时,变量的类型声明需要带有类型参数的值,不过本例等号左边的Box<string>可以省略不写,因为可以从等号右边推断得到。

11.8 抽象类,抽象成员

TypeScript 允许在类的定义前面,加上关键字abstract,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”(abstract class)。

abstract class A {
  id = 1;
}

const a = new A(); // 报错

class B extends A {
  amount = 100;
}

const b = new B();

b.id // 1
b.amount // 100

11.9 this 问题

类的方法经常用到this关键字,它表示该方法当前所在的对象。有些场合需要给出this类型,但是 JavaScript 函数通常不带有this参数,这时 TypeScript 允许函数增加一个名为this的参数,放在参数列表的第一位,用来描述函数内部的this关键字的类型。

class A {
  name = 'A';

  getName() {
    return this.name;
  }
}

const a = new A();
a.getName() // 'A'

13. Enum 类型

13.1 简介

enum Color {
  Red,     // 0
  Green,   // 1
  Blue     // 2
}

let c = Color.Green; // 1
// 等同于
let c = Color['Green']; // 1

let c:Color = Color.Green; // 正确
let c:number = Color.Green; // 正确

// 编译后
let Color = {
  Red: 0,
  Green: 1,
  Blue: 2
};

Enum 结构Color,里面包含三个成员RedGreenBlue。第一个成员的值默认为整数0,第二个为1,第三个为2,以此类推。

Enum 结构本身也是一种类型。比如,上例的变量c等于1,它的类型可以是 Color,也可以是number

由于 TypeScript 的定位是 JavaScript 语言的类型增强,所以官方建议谨慎使用 Enum 结构,因为它不仅仅是类型,还会为编译后的代码加入一个对象。

Enum 结构比较适合的场景是,成员的值不重要,名字更重要,从而增加代码的可读性和可维护性。

13.2 Enum 成员的值

成员的值可以是任意数值,甚至可以相同,但不能是大整数(Bigint)。不能重新赋值。

如果只设定第一个成员的值,后面成员的值就会从这个值开始递增。

enum Color {
  Red = 7,
  Green,  // 8
  Blue   // 9
}

// 或者
enum Color {
  Red, // 0
  Green = 7,
  Blue // 8
}

13.3 同名 Enum 的合并

enum Foo {
  A,
}

enum Foo {
  B = 1,
}

enum Foo {
  C = 2,
}

// 等同于
enum Foo {
  A,
  B = 1C = 2
}

13.4 字符串 Enum

字符串枚举的所有成员值,都必须显式设置。如果没有设置,成员值默认为数值,且位置必须在字符串成员之前。

enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT',
}

// 可以混合赋值
enum Enum {
  One = 'One',
  Two = 'Two',
  Three = 3,
  Four = 4,
}

13.5 keyof 运算符

enum MyEnum {
  A = 'a',
  B = 'b'
}

// 'A'|'B'
type Foo = keyof typeof MyEnum;		// keyof 运算符可以取出 Enum 结构的所有成员名,作为联合类型返回。

13.6 反向映射

可以通过成员值获得成员名。

enum Weekdays {
  Monday = 1,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday,
  Sunday
}

console.log(Weekdays[3]) // Wednesday

14. 类型断言

14.1 简介

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

允许开发者在某个位置“绕过”编译器的类型推断,让本来通不过类型检查的代码能够通过,避免编译器报错。

类型断言有两种语法。

// 语法一:<类型>值
<Type>value

// 语法二:值 as 类型(推荐)
value as Type

// 语法一
let bar:T = <T>foo;

// 语法二
let bar:T = foo as T;

15. 模块

15.1 简介

任何包含 import 或 export 语句的文件,就是一个模块(module)。相应地,如果文件不包含 export 语句,就是一个全局的脚本文件。

模块本身是一个作用域,不属于全局作用域。模块内部的变量、函数、类只在内部可见,对于模块外部是不可见的。暴露给外部的接口,必须用 export 命令声明;如果其他文件要使用模块的接口,必须用 import 命令来输入。

在脚本头部添加

export {};

可以让当前文件当作模块处理,所有它的代码变成内部代码。

// a.ts
type Bool = true | false;

export { Bool };

// b.ts
import { Bool } from './a';	// TypeScript 允许加载模块时,省略模块文件的后缀名,它会自动定位,将./a定位到./a.ts

let foo:Bool = true;

15.2 import type 语句

import 在一条语句中,可以同时输入类型和正常接口,但不利于区分类型和正常接口,容易造成混淆。为了解决这个问题,TypeScript 引入了两个解决方法。

  1. 在 import 语句输入的类型前面加上type关键字。

    import { type A, a } from './a';
    
  2. 使用 import type 语句,这个语句只能输入类型,不能输入正常接口。

    // 正确
    import type { A } from './a';
    
    // 报错
    import type { a } from './a';
    

15.3 importsNotUsedAsValues 编译设置

TypeScript 特有的输入类型(type)的 import 语句,编译成 JavaScript 时怎么处理呢?

TypeScript 提供了importsNotUsedAsValues编译设置项,有三个可能的值。

(1)remove:这是默认值,自动删除输入类型的 import 语句。

(2)preserve:保留输入类型的 import 语句。

(3)error:保留输入类型的 import 语句(与preserve相同),但是必须写成import type的形式,否则报错。

15.4 CommonJS 模块

CommonJS 是 Node.js 的专用模块格式,与 ES 模块格式不兼容。

15.4.1 import = 语句

TypeScript 使用import =语句输入 CommonJS 模块。

import fs = require('fs');
const code = fs.readFileSync('hello.ts', 'utf8');

TypeScript 还允许使用import * as [接口名] from "模块文件"输入 CommonJS 模块。

import * as fs from 'fs';
// 等同于
import fs = require('fs');

15.4.2 export = 语句

TypeScript 使用export =语句,输出 CommonJS 模块的对象,等同于 CommonJS 的module.exports对象。

let obj = { foo: 123 };

export = obj;

export =语句输出的对象,只能使用import =语句加载。

15.5 模块定位

模块定位(module resolution)指的是确定 import 语句和 export 语句里面的模块文件位置。

模块定位有两种方法,一种称为 Classic 方法,另一种称为 Node 方法。可以使用编译参数moduleResolution,指定使用哪一种方法。

没有指定定位方法时,就看原始脚本采用什么模块格式。如果模块格式是 CommonJS(即编译时指定--module commonjs),那么模块定位采用 Node 方法,否则采用 Classic 方法(模块格式为 es2015、 esnext、amd, system, umd 等等)。

15.5.1 相对模块,非相对模块

相对模块指的是路径以/./../开头的模块。下面 import 语句加载的模块,都是相对模块。

  • import Entry from "./components/Entry";
  • import { DefaultHeaders } from "../constants/http";
  • import "/mod";

非相对模块指的是不带有路径信息的模块。下面 import 语句加载的模块,都是非相对模块。

  • import * as $ from "jquery";
  • import { Component } from "@angular/core";

15.5.2 Classic 方法

以当前脚本的路径作为“基准路径”,计算相对模块的位置。

非相对模块,也是以当前脚本的路径作为起点,一层层查找上级目录。

15.5.3 Node 方法

Node 方法就是模拟 Node.js 的模块加载方法。

相对模块依然是以当前脚本的路径作为“基准路径”。

非相对模块则是以当前脚本的路径作为起点,逐级向上层目录查找是否存在子目录node_modules

15.5.4 路径映射

TypeScript 允许开发者在tsconfig.json文件里面,手动指定脚本模块的路径。

  1. baseUrl:可以手动指定脚本模块的基准目录。

    {
      "compilerOptions": {
        "baseUrl": "."
      }
    }
    
  2. paths:指定非相对路径的模块与实际脚本的映射。

    {
      "compilerOptions": {
        "baseUrl": ".",
        "paths": {
          "jquery": ["node_modules/jquery/dist/jquery"]
        }
      }
    }
    
  3. rootDirs:指定模块定位时必须查找的其他目录。

    {
      "compilerOptions": {
        "rootDirs": ["src/zh", "src/de", "src/#{locale}"]
      }
    }
    

15.5.5 tsc 的 --traceResolution 参数

tsc 命令有一个--traceResolution参数,能够在编译时在命令行显示模块定位的每一步。

15.5.6 tsc 的 --noResolve 参数

--noResolve参数,表示模块定位时,只考虑在命令行传入的模块。

17. 装饰器

17.1 简介

装饰器用来在定义时修改类的行为。

  1. 前缀是@,后面是一个表达式。

  2. @后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。

  3. 这个函数接受所修饰对象的一些相关值作为参数。

  4. 这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。

function simpleDecorator(
  value:any,
  context:any
) {
  console.log(`hi, this is ${context.kind} ${context.name}`);
  return value;
}

@simpleDecorator
class A {} // "hi, this is class A"

17.3 装饰器的结构

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,描述这个对象。
    1. kind:字符串,表示所装饰对象的类型,可能取以下的值。
      • 'class'
      • 'method'
      • 'getter'
      • 'setter'
      • 'field'
      • 'accessor'
    2. name:字符串或者 Symbol 值,所装饰对象的名字,比如类名、属性名等。
    3. addInitializer():函数,用来添加类的初始化逻辑。以前,这些逻辑通常放在构造函数里面,对方法进行初始化,现在改成以函数形式传入addInitializer()方法。注意,addInitializer()没有返回值。
    4. private:布尔值,表示所装饰的对象是否为类的私有成员。
    5. static:布尔值,表示所装饰的对象是否为类的静态成员。
    6. access:一个对象,包含了某个值的 get 和 set 方法。

17.4 类装饰器


欢迎关注公众号【消息中间件】(middleware-mq),更新消息中间件的源码解析和最新动态!