TS - 概述

TypeScript 是由微软开发的一种基于 JavaScript 的编程语言,可看成是 JavaScript 的超集。相较于 JavaScript,TypeScript 增加了一个独立的类型系统

类型(type)指的是一组具有相同特征的值,是人为添加的一种编程约束和用法提示。由于变量的类型和对象的属性是动态的,JavaScript 是动态类型语言,与之相反,TypeScript 是静态类型语言

TS - 基本使用

1. 类型声明

  1. 变量类型声明

    1
    let foo: string;

    补充:TypeScript 规定,变量只有赋值后才能使用,否则就会报错

  2. 函数类型声明(参数、返回值)

    1
    2
    3
    function toString(num: number): string {
    return String(num);
    }

2. 类型推断

  1. 变量类型推断

    1
    let foo = 123; // foo 被推断为 number 类型
  2. 函数类型推断(返回值)

    1
    2
    3
    function toString(num: number) { // toString 返回值被推断为 string 类型
    return String(num);
    }

3. 代码运行

3.1 tsc 编译器

  1. 解释:TypeScript 代码只有转换为 JavaScript 代码后,才能在浏览器和 Node.js 中运行,这个过程叫做编译(compile)。TypeScript 官方提供的编译器为 tsc,可以将 .ts 脚本转变为 .js 脚本。

    • TypeScript 编译为 JavaScript 时,会删除全部类型声明和类型相关的代码,只留下可以运行的 JavaScript 代码。
    • TypeScript 的类型检查是编译时的类型检查,而不是运行时的类型检查。
  2. 安装 npm install -g typescript

    补充:安装后,可以使用 tsc -vtsc --version 查看 tsc 版本,从而检查是否安装成功;使用 tsc -htsc --help 查看基本帮助信息,tsc --all 查看完整帮助信息

  3. 编译

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 基本使用
    tsc app.ts # 编译一个 ts 脚本,到当前目录
    tsc file1.ts file2.ts file3.ts # 编译多个 ts 脚本,到当前目录

    # 带编译参数使用
    # --outFile xxx.js 指定编译后的 js 脚本名(默认与源文件一一对应且同名)
    tsc file1.ts file2.ts --outFile app.js # 编译多个 ts 脚本为一个 js 脚本,到当前目录
    # --outDir xxx 指定编译后的 js 脚本的保存位置(默认保存在当前目录)
    tsc app.ts --outDir dist # 编译一个 ts 脚本,到指定目录
    # --target xxx 指定编译后的 js 脚本对应的版本(默认编译为低版本的 JavaScript,兼容性考量)
    tsc --target es2015 app.ts # 编译一个 ts 脚本,到当前目录,同时控制编译后的 JavaScript 版本
    # --noEmitOnError 一旦报错就停止编译,同时不生成编译产物(默认情况不会停止编译,且会生成编译产物)
    tsc --noEmitOnError app.ts # 编译一个 ts 脚本,到当前目录,如果报错就停止编译,且不生成编译产物
    ## --noEmit 只检查类型是否正确,不生成 js 脚本
    tsc --noEmit app.ts # 编译一个 ts 脚本,但是不生成 js 脚本

    补充:TypeScript 允许使用配置文件 tsconfig.json 存储 tsc 的编译参数。只要当前目录有该文件,那么 tsc 就会在编译时自动读取,而不需要再显式地指定参数。

    依赖追踪:编译脚本时,tsc 会自动编译脚本所依赖的所有脚本。

    注意:tsc app.ts 时,编译器忽略任何 tsconfig.json,使用默认编译选项!!!

3.2 ts-node 模块

  1. 解释:ts-node 是一个非官方的,可以直接运行 TypeScript 代码的模块。

  2. 安装 npm install -g ts-node

  3. 使用

    1
    2
    3
    ts-node script.ts # 方式一:运行 ts 脚本 script.ts
    npx ts-node script.ts # 方式二:不安装 ts-node 时,使用 npx 来在线运行 ts 脚本
    ts-node # 方式三:打开一个 ts 脚本地 REPL 运行环境

    注意:使用 ts-node 运行 ts 脚本时,需要全局安装 typescript 包,并在项目根目录下添加配置文件 tsconfig.json

    注意:使用 ts-node 运行 ts 脚本时,且该 ts 脚本是一个 ES6 模块,那么可能会报错!!这是因为 Node.js 支持 ES6 模块是一个实验性的功能,而 ts-node 是根据 Node.js 版本进行适配的。因此一个可行的策略是降低 Node.js 版本到 v18.17.1。(最好在 tsconfig.json 中也补充了配置 { "ts-node": { esm: true } },以使得 ts-node 以 ES6 模块化解析 ts 脚本)

TS - 类型系统 1(特殊、基本、包装、Object、值、联合、交叉、type、typeof、作用域、兼容)

1. 特殊类型

1.1 any

  1. 解释:any 类型表示没有任何限制。一旦将变量类型设为 any,TypeScript 就会关闭对该变量的类型检查,并且此变量可以被赋予任意类型的值,一般不推荐使用该类型。any 类型可以看成是所有其他类型的全集,因此 TypeScript 将该类型称为顶层类型(top type)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let x: any;

    /* 变量的类型检查被关闭(往往不是我们所希望的) */
    x(1); // √
    x.foo = "hello"; // √

    /* 变量可以被赋予任意类型的值 */
    x = 1; // √
    x = true; // √
  2. 适用场景

    • 出于某些特殊原因,需要关闭部分变量的类型检查。
    • 为适配老的 JavaScript 项目,使代码能快速迁移到 TypeScript 。
  3. 存在的问题(不建议使用 any 类型的原因)

    • 类型推断:我们知道,TypeScript 会推断那些没有进行类型声明的变量,如果无法推断,TypeScript 就会将相应变量的类型认定为 any。为了避免这种问题,可以在使用 tsc 编译器进行编译时,使用编译参数 --noImplicitAny,此时只要推断出 any 类型就会报错。但需要注意的是,使用 letvar 声明变量时,如果不赋值,也不指定类型,TypeScript 就会推断其类型为 any,且不会报错,为了解决这个安全隐患,使用 letvar 声明变量时,要么赋值,要么显式声明类型

      1
      const add = (x, y) => x + y; // 函数参数和返回值都被推断为 any 类型,可能会导致错误
      1
      tsc --noImplicitAny app.ts # 编译 app.ts 脚本,当推断出 any 类型的变量时报错
    • 类型污染any 类型的变量能够赋值给其他任何类型的变量,进而污染具有正确类型的变量,把错误留到了运行时

      1
      2
      3
      4
      let x: any = "hello";
      let y: number;

      y = x; // 类型污染,number 类型的变量 y 此时的值是一个字符串,可能会导致错误

1.2 unknown

  1. 解释:unknown 类型可被视作严格版的 any 类型 ,它与 any 类型存在相似之处,同时也增添了一些限制。unknown 类型可以看成是所有其他类型(除了 any 类型)的全集,因此 TypeScript 也将该类型称为顶层类型(top type)。

    • 相似之处:所有类型的值均可赋值给 unknown 类型的变量

      1
      2
      3
      4
      let x: unknown;

      x = true; // √
      x = 1; // √
    • 限制之处

      • unknown 类型的变量不能直接赋值给其他类型的变量(除了 anyunknown 类型),从而避免了 any 类型中存在的类型污染问题。

        1
        2
        3
        let v: unknown = 123;

        let v1: boolean = v; // ×
      • unknown 类型的变量的属性和方法不能直接被调用,从而避免了 any 类型的类型检查关闭所带来的问题。

        1
        2
        3
        let v: unknown = { foo: 123 };

        v.foo; // ×
      • unknown 类型的变量只能进行有限类型的运算,如 =====!=!==||&&?!typeofinstanceof

        1
        2
        3
        4
        let v: unknown = 1;

        a + 1; // ×
        a === 1; // √
      • unknown 类型的变量只有经过类型缩小才可以使用,所谓类型缩小,就是缩小 unknown 类型变量的类型范围,以确保不会出错。通常使用条件判断语句和 typeof 运算符来缩小 unknown 类型变量的类型范围。

        1
        2
        3
        4
        5
        6
        7
        let a: unknown = 1;

        if (typeof a === 'number') {
        let r = a + 10; // ✓
        } else if (typeof a === 'string') {
        console.log(a.length); // ✓
        }
  2. 适用场景:unknown 类型可以被看作是更安全的 any 类型,因此凡是需要设定为 any 类型的变量,通常都应优先考虑设为 unknown 类型

1.3 never

解释:never 类型表示空类型,即该类型不包含任何值never 类型可以看成是空集,因此该类型是任何其他类型所共有的,TypeScript 将该类型称为底层类型(bottom type)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 不可以赋任何值给 never 类型的变量 */
let x: never;

/* 场景1:对于不可达的条件分支,其变量类型可能是 never */
const fn = (x: string | number) {
if (typeof x === 'string') {
// ...
} else if (typeof x === 'number') {
// ...
} else {
x; // never 类型
}
}

/* 场景2:对于不可能返回值的函数(如报错),其返回值的类型是 never */
const fn2 = (): never => {
throw new Error("Error");
}

/* never 类型是其他类型所共有的 */
let v1: number = fn2();
let v2: boolean = fn2();

2. 基本类型

类型 可取值 类型声明
boolean truefalse const x: boolean = true
string 所有字符串 const x: string = 'hello'
number 所有整数和浮点数 const x: number = 0xffff
bigint 所有大整数 const x: bigint = 123n
symbol 所有的 Symbol const x: symbol = Symbol();
object 所有的对象、数组和函数 const x: object = { foo: 123 }
const y: object = [1, 2, 3]
const z: object = (n: number) => n + 1
undefined undefined,表示未定义 const x: undefined = undefined
null null,表示空 const x: null = null

补充说明

  • bigint 类型与 number 类型不兼容
  • bigint 类型由 ES2020 标准引入,如果使用该类型,tsc 编译器的目标 JavaScript 版本不能低于 ES2020(即 target 参数不低于 es2020)。
  • undefinednull 即可以作为,也可以作为类型
  • undefinednull 能够赋值给其他任何类型的变量。可以使用 --strictNullChecks 编译选项来规避这种情况,此时 undefinednull 无法赋值给其他类型的变量了(除了 anyunknown 类型的变量)
  • 当一个没有声明类型的变量被赋值 undefinednull 时,不同的编译选项可能会导致不同的类型推断
    • 关闭 --noImplicitAny--strictNullChecks,变量被推断为 any
    • 打开 --strictNullChecks,值为 undefined 的变量被推断为 undefined 类型,值为 null 的变量被推断为 null 类型。

3. 包装对象类型

  1. 原始类型(primitive type):表示最基本的、不可再分的值,包括:booleanstringnumberbigintsymbol

  2. 包装对象(wrapper object):五种原始类型的值均有其对应的包装对象,也就是原始类型的值在必要时会自动转换而成的对象。

    补充:可以通过 new Boolean()new String()new Number() 的方式获取 booleanstringnumber 类型的值所对应的包装对象;可以通过 Object(Symbol())Object(BigInt()) 的方式获取 symbolbigint 类型的值对应的包装对象。

    补充:原始类型的值及其包装对象又可称之为字面量包装对象,如 "hello" 是字面量,其包装对象为 new String("hello")

  3. 包装对象类型:五种原始类型对应的包装对象的类型,用大写表示,包括 BooleanStringNumberBigIntSymbol包装对象类型包含包装对象和字面量两种情况,原始类型只包含字面量

    1
    2
    3
    4
    5
    const s1: String = 'hello'; // √ 包装类型 - 字面量
    const s2: String = new String('hello'); // √ 包装类型 - 包装对象

    const s3: string = 'hello'; // √ 原始类型 - 字面量
    const s4: string = new String('hello'); // × 原始类型 - 包装对象

    补充:建议只使用原始类型,不使用包装对象类型。

    补充:symbolSymbol 之间,以及 bigintBigInt 之间没有差异。

4. Object/object

  1. Object 类型:表示 JavaScript 中的广义对象,即所有能够转变为对象的值皆为 Object 类型(除了 undefinednull)。空对象 {}Object 类型的简写形式。

    补充:Object 类型的对象可以接受各种类型的属性,但是不能读取,否则会报错。

  2. object 类型:表示 JavaScript 中的狭义对象,仅包含对象、数组和函数。

5. 值类型

  1. 值类型:将单个值作为一种类型,称其为 “值类型”。当使用 const 声明变量,并给该变量赋一个原始值时,TypeScript 就会推断该变量的类型为值类型。

    1
    const x = 'https'; // x 的类型被自动推断为 "https"

    补充:如果 const 声明的变量所赋的值为对象,那么 TypeScript 则不会推断该变量的类型为值类型。

  2. 与值类型有关的报错

    1
    2
    3
    4
    const x: 5 = 4 + 1; // ×,因为 x 的类型被认定是 5,而 4 + 1 的类型被推测为 number,5 是 number 的子类型,number 是 5 的父类型,父类型不能赋值给子类型,子类型能够赋值给父类型。在此由于将 number 这个父类型赋值给 5 这个子类型,所以发生报错。

    /* 修正方式:使用 “类型断言”,即告诉 TypeScript 4 + 1 的类型是 5,而不是 number */
    const x: 5 = (4 + 1) as 5; // √

6. 联合类型

  1. 解释:多个类型通过符号 | 能够组成一个新的类型,称作联合类型(union types)。任意一个值只要属于 A 类型或者 B 类型,那么也就属于联合类型 A | B,其中 AB 是合法的 TypeScript 类型。

  2. 语法

    • 单行书写

      1
      let x: A | B | C | ...;
    • 多行书写

      1
      2
      3
      4
      5
      let x: 
      | A
      | B
      | C
      | ...;

7. 交叉类型

  1. 解释:多个类型通过符号 & 能够组成一个新的类型,称作交叉类型(intersection tyoe)。任意一个值同时属于 A 类型 B 类型,那么也就属于交叉类型 A & B,其中 AB 是合法的 TypeScript 类型。

  2. 语法

    1
    let x: A & B & C & ...;
  3. 适用场景:为对象类型添加新属性

    1
    2
    3
    type A = { foo: number }; // A 类型表示具有 number 类型的 foo 属性的对象

    type B = A & { bar: number }; // B 类型对 A 类型进行扩展,要求对象还要有 number 类型的 bar 属性

8. 类型别名 - type

  1. 解释:TypeScript 中允许通过 type 关键字来定义一个类型的别名

    1
    2
    type Gender = "Male" | "Female"; // Gender 类型,取值范围为 "Male" 和 "Female"
    type Weather = "Sunny" | "Clear" | "Rainy";
  2. 注意事项

    • 类型别名的作用域是块级作用域

      1
      2
      3
      4
      type Color = 'red';
      if (Math.random() < 0.5) {
      type Color = 'blue'; // ×
      }
    • 在同一作用域中,类型别名不允许重名

      1
      2
      type Color = 'red';
      type Color = 'blue'; // ×
    • 类型别名允许嵌套

      1
      2
      3
      4
      5
      type Country = "China";
      type Province = "Shaan'xi";
      type Address = `Xi'an ${Province} ${Country}`;

      let x: Address = "Xi'an Shaan'xi China";

9. 类型运算 - typeof

  1. 解释:typeof 运算符在 JavaScript 和 TypeScript 中具有不同的功能。在 JavaScript 中,typeof 接受一个操作数,返回一个字符串,代表操作数的类型。在 TypeScript 中,typeof 也接受一个操作数,但是返回的是操作数的 TypeScript 类型。前者称之为值运算,后者称之为类型运算

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /* 值运算 - JavaScript 中的 typeof */
    typeof undefined; // "undefined"
    typeof true; // "boolean"
    typeof 13; // "number"
    typeof "foo"; // "string"
    typeof {}; // "object"
    typeof parseInt; // "function"
    typeof Symbol(); // "symbol"
    typeof 13n; // "bigint"

    /* 类型运算 - TypeScript 中的 typeof */
    const a = { x: 0};

    type T0 = typeof a; // { x: number }
    type T1 = typeof a.x; // number
  2. 注意事项

    • TypeScript 代码中,可能同时存在两种 typeof 运算符,即一种用于值运算,一种用于类型运算

      1
      2
      3
      4
      5
      let a = 1;
      let b: typeof a; // 类型运算

      if(typeof a === 'number')
      b = a; // 值运算
    • TypeScript 代码编译后,用于值运算typeof 运算符会被保留,用于类型运算typeof 运算符会被全部删除

      1
      2
      3
      4
      5
      6
      /* 上一个 ts 示例代码的编译结果 */
      let a = 1;
      let b; // 类型运算 - typeof 被删除

      if(typeof a === 'number')
      b = a; // 值运算 - typeof 被保留
    • TypeScript 代码在编译时不会进行 JavaScript 的值运算,所以用于类型运算typeof 运算符的操作数不能是需要进行运算的表达式。此外,用于类型运算typeof 操作符的操作数不能是类型

      1
      2
      3
      4
      type T = typeof Date(); // ×

      type Age = number;
      type MyAge = typeof Age; // ×

10. 类型作用域 - 块级

解释:TypeScript 支持块级类型声明,即类型可以声明在代码块中,并且只在当前代码块中生效。

11. 类型兼容

解释:TypeScript 中规定子类型兼容父类型,凡是可以使用父类型的地方,都可以使用子类型,反之则不行。如 number | string 就是 number 的父类型。

TS - 类型系统 2(数组、元组、symbol)

1. 数组

1.1 基本使用

  1. 数组(array):TypeScript 中的数组规定所有成员的类型必须相同,成员数量不定

  2. 语法

    • elementType[](elementType)[]

      1
      2
      let arr1: number[] = [1, 2, 3];
      let arr2: (number | string)[] = [1, "hello", 3]; // 考虑到运算符优先级,复杂的成员类型需要写在括号中
    • Array<elementType>

      1
      2
      let arr1: Array<number> = [1, 2, 3];
      let arr2: Array<number | string> = [1, "hello", 3];
  3. 注意事项

    • 数组成员的数量可以动态变化,因此对数组进行越界访问并不会报错

    • TypeScript 使用 elementType[][] 的形式表示二维数组

    • TypeScript 允许通过索引读取数组成员的类型,也就是对数组类型进行索引访问。

      1
      2
      3
      4
      5
      6
      7
      /* 情况一:访问指定索引的成员类型 */
      type Names = string[];
      type Name = Names[0]; // string

      /* 情况二:访问指定类型的索引的成员类型 */
      type Names2 = string[];
      type Name2 = Names2[number]; // string
    • 当变量的初始值为空数组时,TypeScript 会推断该变量的类型为 any[] ,之后每当给该数组赋值,TypeScript 都会自动更新对该变量的类型推断

      1
      2
      3
      const arr = []; // 推断类型为 any[]
      arr.push(1); // 推断类型为 number[];
      arr.push("HELLO"); // 推断类型为 (string | number)[]

1.2 只读数组

  1. 只读数组:相较于普通数组,TypeScript 规定只读数组没有 pop()push() 等会改变原数组的方法,因此也称只读数组为普通数组的父类型(成员类型需相同),普通数组是只读数组的子类型

  2. 语法

    • readonly elementType[]readonly (elementType)[]

    • ReadonlyArray<elementType>

    • Readonly<elementType[]>

    • [] as constconst 断言,只读的值类型

      1
      2
      3
      4
      let arr1: readonly number[] = [1, 2, 3];
      let arr2: ReadonlyArray<number> = [1, 2, 3];
      let arr3: Readonly<number[]> = [1, 2, 3];
      let arr4 = [1, 2, 3] as const; // 不能写成 let arr4: number[] = [1, 2, 3] as const !
  3. 注意事项:由于只读数组是普通数组的父类型(成员类型相同),因此如果在需要普通数组的地方使用只读数组,则应使用 as 关键字进行类型断言。

    1
    2
    3
    const func = (arr: number[]) => {}
    const arr: readonly number[] = [1, 2, 3];
    func(arr as number[]); // 父类型 readonly number[],子类型 number[]

2. 元组

2.1 基本使用

  1. 元组(tuple):TypeScript 中的元组就是成员类型可以自由设置的数组,因此元组的每个成员的类型都必须明确声明

  2. 语法

    • [elementType1, elementType2, elementType3]

    • [elementType1, elementType2, elementType3?]可选成员,必须位于必选成员之后)

    • [elementType1, ...elementType2, elementType3[]]不限数量的成员,可以位于任意位置)

    • [elementName1: elementType1, elementName2: elementType2, elementName3: elementType3]指定成员名,仅起到说明作用)

      1
      2
      3
      4
      const t1: [number, string, boolean] = [1, "hello", true];
      const t2: [number, string, boolean?] = [1, "hello"];
      const t3: [number, ...string[], boolean] = [1, "hello", "world", true];
      const t4: [num: number, msg: string, flag: boolean] = [1, "hello", true];
  3. 注意事项

    • TypeScript 中数组的成员类型写在方括号外(如 number[]),元组的成员类型写在方括号里(如 [number])。

    • 使用元组时必须给出类型声明,不然 TypeScript 会将其自动推断为一个数组。

    • TypeScript 会根据元组的成员类型推断成员数量(或元组长度)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      /* 1. 元组没有可选成员和扩展运算符,TypeScript 会推断出元组的成员数量 */
      let t1: [number, number] = [0, 1];
      t1.length === 3; // ×,只能是 2

      /* 2. 元组包含可选成员,TypeScript 会推断出元组的可能的成员数量 */
      let t2: [number, number?] = [0];
      t2.length === 3; // ×,可以是 1 或 2

      /* 3. 元组中包含使用了扩展运算符的数组或元组,TypeScript 无法推断出元组的成员数量 */
      let t3: [...number[]] = [1, 2, 3];
      t3.length === 9; // √
    • 扩展运算符可以将数组或元组转换为一个逗号分隔的序列。TypeScript 无法确定数组转换的序列的成员数量,但是可以确定元组转换的序列的成员数量,以及 const 断言的值类型转换的序列的成员数量。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      const func = (a: number, b: number) => { }

      /* 1. 数组转换的序列的成员数量无法确定 */
      let a: number[] = [1, 2, 3];
      func(...a); // ×,因为 TypeScript 无法确定 a 转换的序列的成员数量为 2

      /* 2. 元组转换的序列的成员数量可以确定 */
      let b: [number, number] = [1, 2];
      func(...b); // √,因为 TypeScript 可以确定 b 转换的序列的成员数量为 2


      /* 3. const 断言转换的序列的成员数量可以确定 */
      let c = [1, 2] as const; // c 的 类型为 readonly [1, 2]
      func(...c);; // √,因为 TypeScript 可以确定 c 转换的序列的成员数量为 2

      [] as const 断言的类型是只读的值类型,即可以当作只读数组,也可以当作只读元组

    • 因为在使用元组时需要声明每个成员的类型,大多数情况下元组的成员数量有限,所以此时不允许对元组进行越界访问

      1
      2
      3
      const t: [number, boolean, string] = [1, true, "hello"];
      t[1]; // √
      t[3]; // ×
    • TypeScript 允许通过索引读取元组成员的类型,也就是对元组类型进行索引访问。

      1
      2
      3
      4
      5
      6
      7
      /* 情况一:访问指定索引的成员类型 */
      type Eles = [string, number];
      type Ele = Eles[1]; // number

      /* 情况二:访问指定类型的索引的成员类型 */
      type Eles2 = [string, number];
      type Ele2 = Eles2[number]; // string | number

2.2 只读元组

  1. 只读元组:与只读数组类似,只读元组是普通元组的父类型(成员类型需对应相同),普通元组是只读元组的子类型

  2. 语法

    • readonly [elementType1, elementType2, elementType3]

    • Readonly<[elementType1, elementType2, elementType3]>

    • [] as constconst 断言,只读的值类型)

      1
      2
      3
      let t1: readonly [number, boolean] = [1, true];
      let t2: Readonly<[number, boolean]> = [1, true];
      let t3 = [1, true] as const;
  3. 注意事项:由于只读元组是普通元组的父类型(成员类型需对应相同),因此如果在需要普通元组的地方使用只读元组,则应使用 as 关键字进行类型断言。

    1
    2
    3
    const func = (t: [number, number]) => { };
    const t: readonly [number, number] = [1, 2];
    func(t as [number, number]);

3. symbol

3.1 基本介绍

JavaScript 中的 Symbol 是 ES2015 引入的一种新的原始类型的值,其特点是:每一个 Symbol 值都是唯一的,与其他任何值都不相等。

JavaScript 中的 Symbol 值可以通过 Symbol() 函数生成。也可以通过 Symbol.for(key: string) 函数生成,相同的 key 对应相同的 Symbol 值。

TypeScript 中使用 symbol 类型来表示 Symbol 值。

3.2 unique symbol

  1. 解释:unique symbolsymbol 的一个子类型,用于表示单个的,某个具体的 Symbol 值。类型为 unique symbol 的变量只能使用 const 命令声明。

    1
    2
    const x: unique symbol = Symbol(); // √
    let y: unique symbol = Symbol(); // ×
  2. 注意事项

    • 使用 const 关键字声明变量,并赋 Symbol 值时,变量类型被自动推断为 unique symbol。可以简化 unique symbol 类型的变量声明。

      1
      const z = Symbol(); // 等价于 const z: unique symbol = Symbol();
    • 每个声明为 unique symbol 类型的变量的值不同,类型也不同

      1
      2
      3
      4
      const a: unique symbol = Symbol();
      const b: unique symbol = Symbol();

      a === b; // ×
    • 因为 Symbol.for(key)key 相同时返回相同的 Symbol 值,因此可能会导致多个 unique symbol 类型的变量的值相同

      1
      2
      3
      4
      const p: unique symbol = Symbol.for('hello');
      const q: unique symbol = Symbol.for('hello');

      p === q; // 虽然 p 和 q 的值相同,但是其类型不同,因此不能作 === 运算
    • unique symbol 类型是 symbol 类型的子类型,因此在使用 symbol 的时候可以使用 unique symbol,反之则不行。

    • unique symbol 类型的值可以作为对象的属性名,而 symbol 类型的值不行。

      1
      2
      3
      4
      5
      6
      7
      const m: unique symbol = Symbol();
      let n: symbol = Symbol();

      type Foo = {
      [m]: string; // unique symbol 可以作为属性名
      [n]: string; // symbol 不可以作为属性名
      }
    • 可以通过 static readonly fieldName: unique symbol = Symbol() 的方式声明一个类型为 unique symbol静态只读属性

      1
      2
      3
      class Person {
      static readonly foo: unique symbol = Symbol();
      }
    • Symbol() 函数的值赋给一个 let 声明的变量,其类型将会被推断为 symbol;赋给一个 const 声明的变量,其类型将会被推断为 unique symbol

      1
      2
      let j = Symbol(); // symbol
      const k = Symbol(); // unique symbol
    • symbolunique symbol 类型的变量赋值给 constlet 声明的变量,其类型都会被推断为 symbol

      1
      2
      3
      4
      5
      let e = Symbol();
      const f = e; // symbol

      const g = Symbol();
      let h = g; // symbol

TS - 类型系统 3(函数、对象)

1. 函数

1.1 基本使用

  1. 解释:函数类型声明,即在声明函数时,给出参数的类型和返回值的类型。

  2. 语法

    • function(para1: paraType1, para2: paraType2, ...): returnType{}(函数声明)

      1
      const speakHi = function (msg: string): void { console.log(msg) }
    • (para1: paraType1, para2: paraType2,...) => returnType(函数类型,一般写法)

      1
      const speakHello: (msg: string) => void = function (msg) { console.log(msg) }
    • { (para1: paraType1, para2: paraType2,...): returnType }(函数类型,对象写法)

      1
      const speakWelcome: { (msg: string): void } = function (msg) { console.log(msg) }

      补充:当函数本身存在属性时,多使用这种函数类型声明方式

    • type FuncTypeName = (para1: paraType1, para2: paraType2,...) => returnTypetype FuncTypeName = { (para1: paraType1, para2: paraType2,...): returnType }(函数类型,type 写法)

      1
      2
      type func = { (msg: string): void }
      const speakWelcome: func = function (msg) { console.log(msg) }
    • interface FuncTypeName { (para1: paraType1, para2: paraType2,...): returnType }(函数类型,interface 写法)

      1
      2
      interface IFunc { (msg: string): void };
      const speakYes: IFunc = function (msg) { console.log(msg) }
  3. 使用说明

    • 如果不指定参数类型,TypeScript 会推断参数类型为 any;返回值类型通常可以被 TypeScript 自动推断而得出。

    • 函数类型中的参数名称可以与实际函数的参数名称不一致

      1
      const printf: (a: any, b: any) => void = (para1, para2) => { console.log(para1, para2) }
    • 函数类型中的参数数量可以多于实际函数的参数数量,但不能小于。

      1
      2
      3
      type func = (a: any, b: any) => void
      const printf1: func = (para1) => { console.log(para1) } // √,实际函数参数数量可以少于
      const printf2: func = (para1, para2, para3) => { console.log(para1, para2, para3) } // ×,实际函数参数数量不能多余!
    • 可以使用 typeof 运算符计算一个函数变量的函数类型,并将其用作另一个变量的类型声明。

      1
      2
      const add: (a: number, b: number) => number = (a, b) => a + b;
      const del: typeof add = (a, b) => a - b;
    • 函数内部允许声明其他类型,并只在该函数内部有效,称之为局部类型

    • 如果一个函数的返回值是一个函数,那么该函数称之为高阶函数(higher-order function)。

1.2 Function

Function 也是一种函数类型,任何函数都归属于该类型。Function 表示对函数不做任何约束,接受任意数量的参数,参数和返回值的类型皆为 any

1.3 特殊参数

可选参数

解释:参数名后加问号(?)意味着该参数为可选参数,即调用函数时该参数能够省略。

1
2
3
4
const sum: (a: number, b?: number) => number = (x, y) => {
if (y === undefined) return x
else return x + y;
}
  • 函数的可选参数只能处于参数列表的尾部,在必选参数之后。
  • 可选参数的类型实际上为原始类型|undefined
参数解构

解释:元组参数或对象参数在函数的参数列表中被拆解为若干参数,称其为参数解构。

1
2
3
4
5
6
7
8
9
/* 解构元组 */
function func1([x, y]: [number, string]): void { console.log(x, y) };

/* 解构对象 */
function func2({ name, age, gender }: { name: string, age: number, gender: string }): string { return name + age + gender }

/* 解构对象(对象类型由 type 别名定义) */
type Stu = { name: string, age: number, grade: number, className: number };
function func3({ name, age, grade, className }: Stu): string { return name + age + grade + className }
rest 参数

解释:参数名前加省略号(...)意味着该参数为 rest 参数,表示函数中剩余的所有参数,该参数可分为数组(剩余参数类型相同)和元组(剩余参数类型不同)两种类型。其中,元组类型的 rest 参数需要声明每一个成员的类型,同时元组中的成员也可以标记为可选的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 数组类型的 rest 参数 */
function func1(a: number, ...b: number[]): void { console.log(a, b) };
func1(1); // 1, []
func1(1, 2); // 1, [2]
func1(1, 2, 3); // 1, [2, 3]

/* 元组类型的 rest 参数 */
function func2(a: number, ...b: [number, string]): void { console.log(a, b) };
func2(1); // ×
func2(1, 2); // ×
func2(1, 2, "3"); // 1, [2, "3"]

/* 元组类型的 rest 参数,包含可选的成员 */
function func3(a: number, ...b: [number, string?]): void { console.log(a, b) };
func3(1); // ×
func3(1, 2); // 1, [2]
func3(1, 2, "3"); // 1, [2, "3"]

/* 元组类型的 rest 参数 + 参数解构 */
function func4(a: number, ...[b1, b2]: [number, string]): void { console.log(a, b1, b2) };
func4(1, 2, "3"); // 1, 2, "3"
只读参数

解释:参数类型前边加上 readonly 关键字意味着该参数为只读参数。目前,readonly 关键字只允许在数组和元组类型的参数上应用。

1
2
3
function sum(arr: readonly number[]): number {
return arr.reduce((prev, cur) => prev + cur)
};

1.4 特殊返回值

void

解释:该类型表示函数没有返回值

  • void 类型允许函数返回 undefinednull。但如果打开了 strictNullChecks 编译选项,此时函数只能返回 undefined,否则会报错。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function func1(): void {
    return undefined; // √
    }
    function func2(): void {
    return null; // 如果打开 strictNullChecks 编译选项,则会报错
    }
    function func3(): void {
    return 123; // ×
    }
  • 如果变量、对象方法、函数参数的类型是一个返回值为 void 类型的函数,此时可以给其赋值一个有返回值的函数,并且不会报错。这是因为 TypeScript 认为此时的 void 类型表示该函数的返回值没有利用价值,或者不应该使用该函数的返回值。因此,一旦使用了该函数的返回值,就会报错

    联想:这样设计是具有现实意义的,比如数组方法 Array.prototype.forEach(fn) 的参数 fn 就是一个返回值为 void 的函数,其可以被赋值为一个有返回值的函数,其返回值不会被使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /* 返回值为 void 类型的函数变量被赋值为一个有返回值的函数 */
    const func1: () => void = function () { return 123 };
    /* 返回值为 void 类型的对象方法被赋值为一个有返回值的函数 */
    class Person {
    func2: () => void = function () { return 123 }
    }
    /* 返回值为 void 类型的函数参数被赋值为一个有返回值的函数 */
    function log(func3: () => void) { func3() };

    /* void 类型的函数,表示其返回值不应该被使用,否则就会报错 */
    func1() * 2; // ×
    (new Person()).func2() * 2; // ×
  • 如果函数字面量的类型是一个返回值为 void 类型的函数,此时是不能有返回值的(undefinednull 除外)。

    1
    function func(): void { return 123 }; // ×
  • 如果函数在运行中必定会报错,则可以将返回值类型写为 void

  • 如果将一个变量类型设置为 void,此时只能将其赋值为 undefinednull(没有打开 strictNullChecks 编译选项)。

never

解释:该类型表示肯定不会出现的值。如果函数抛出异常陷入了死循环,那么该函数就无法正常返回一个值,此时函数的返回值类型就是 never

1
2
3
4
5
6
7
8
9
/* 函数抛出异常 */
function func1(): never {
throw new Error('never-test');
}

/* 函数陷入死循环 */
function func2(): never {
while (true) { console.log('never-test') };
}
  • 函数返回值类型为 never,表示函数无法正常执行结束,不可能有返回值;函数返回值类型为 void,表示函数可以正常执行结束,但是不返回值(或者返回 undefined)。

  • 程序中调用一个返回值类型为 never 的函数,会导致程序在该函数调用的位置终止,即不会继续执行后续的代码。

  • 如果函数在某些情况下能够正常执行并返回值,在另一些情况下无法正常执行,此时其返回值类型可以是正常执行情况下的返回值。

    1
    2
    3
    4
    5
    6
    7
    8
    /* 以下函数的返回值类型实际上为 never|number,但是由于 never 是底层类型,never|number 等价于 number */
    function func(): number {
    if (Math.random() > 0.5) {
    console.log("Success");
    return 200;
    }
    throw new Error("Error");
    }

1.5 函数重载

  1. 解释:有些函数(包含对象的方法)可以根据参数类型个数不同而执行不同逻辑,称之为函数重载(function overload)。函数重载该有利于精确描述函数参数与返回值之间的对应关系。

  2. 语法:为了实现函数重载,TypeScript 要求提供多个函数重载声明一个函数具体实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function func(a: paraType1, b: par2Type2): returnType1; // 重载声明 1
    function func(a: paraType3, b: paraType4): returnType2; // 重载声明 2
    /*
    重载声明的对象写法
    type func = {
    (a: paraType1, b: par2Type2): returnType1;
    (a: paraType3, b: paraType4): returnType2;
    }
    */
    function func(a: paraType1 | paraType2, b: paraType2 | paraType4): returnType1 | returnType2 { // 具体实现
    if (typeof paraType1 === "paraType1" && typeof paraType2 === "paraType2") {
    /* 重载声明 1 的具体实现 */
    }

    if (typeof paraType3 === "paraType3" && typeof paraType4 === "paraType4 ") {
    /* 重载声明 2 的具体实现 */
    }

    throw new Error("非法的函数参数");
    }
  3. 使用说明

    • 函数的重载声明和函数的具体实现之间不能包含其他代码,否则会报错。
    • 因为 TypeScript 按顺序从上到下检查函数的重载声明,因此要将类型最宽的重载声明放在最下边,以防覆盖其他类型声明。
    • 不使用重载声明也能够实现函数重载,但是此时会丧失参数与返回值之间存在的对应关系,所以如果参数的选择不影响返回值类型时,可以省略重载声明。

1.6 构造函数

构造函数的类型声明

  • 语法1 type ConstructorTypeName = new (para1: paraType1, para2: paraType2, ...) => ObjectType
  • 语法2 type ConstructorTypeName = { new (para1: paraType1, para2: paraType2, ...): ObjectType}

补充:如果一个函数即可以当作构造函数使用,又可以当作普通函数使用,则可以使用以下语法进行类型声明

1
2
3
4
type FuncTypeName = { 
new (para1: paraType1, para2: paraType2, ...): ObjectType; // 构造函数类型
(para1: paraType1, para2: paraType2, ...): returnType; // 普通函数类型
}

2. 对象

2.1 基本使用

  1. 解释:对象类型声明,即在声明对象时,指定对象属性的类型和方法的类型。

  2. 语法

    • 一般写法

      1
      2
      3
      4
      5
      6
      /* 语法 */
      {
      propertyName: propertyType; // 属性
      methodName1: (para1: paraType1, para2: paraType2) => returnType1; // 方法 - 方式 I
      methodName2(para1: paraType1, para2: paraType2): returnType2; // 方法 - 方式 II
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      /* 示例 */
      const person: {
      name: string;
      age: number;
      speak(): void;
      introduce: () => void;
      } = {
      name: "Jack",
      age: 19,
      speak() { console.log("Hi! Nice to meet you~") },
      introduce: () => { console.log("My name is Jack and I'm 19 years old!") },
      }
    • type 写法(即给对象类型起一个别名)

      1
      2
      3
      4
      5
      6
      /* 语法 */
      type ObjectTypeName = {
      propertyName: propertyType; // 属性
      methodName1: (para1: paraType1, para2: paraType2) => returnType1; // 方法 - 方式 I
      methodName2(para1: paraType1, para2: paraType2): returnType2; // 方法 - 方式 II
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      /* 示例 */
      type Animal = {
      name: string;
      weight: number;
      bark(): void;
      }
      const dog: Animal = {
      name: "Lucky",
      weight: 9,
      bark() { console.log("WangWang!") }
      }
    • interface 写法(即将对象类型提炼为一个接口)

      1
      2
      3
      4
      5
      6
      /* 语法 */
      interface ObjectTypeName {
      propertyName: propertyType; // 属性
      methodName1: (para1: paraType1, para2: paraType2) => returnType1; // 方法 - 方式 I
      methodName2(para1: paraType1, para2: paraType2): returnType2; // 方法 - 方式 II
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      /* 示例 */
      interface IStudent {
      id: number;
      className: number;
      grade: number;
      welcome: () => void;
      }
      const stu: IStudent = {
      id: 121313,
      className: 12,
      grade: 3,
      welcome() { console.log("Good Morning~") }
      }
  3. 使用说明

    • TypeScript 使用大括号 "{}" 表示对象类型,同时在大括号内部声明每个属性和方法的类型。每个属性/方法的类型可以以分号 ";" 或逗号 "," 结尾,最后一个属性/方法后边可以省略分号或逗号。

    • 与数组类似,TypeScript 允许通过属性/方法名读取对应属性/方法的类型,也就是对对象类型进行索引访问。

      1
      2
      3
      4
      5
      type User {
      name: string,
      age: number
      };
      type Name = User['name']; // string
    • 在 TypeScript 里,若想在对对象进行解构赋值时明确解构变量的类型,得给所有要解构的变量添加上对象类型声明,不能直接给解构变量加类型,因为直接加类型的语法是用来设置解构变量别名的。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      const {
      x: user_name, // 给解构变量 x 设置别名
      y: user_age // 给解构变量 y 设置别名
      }: {
      x: string, // 给解构变量 x 声明类型
      y: string // 给解构变量 y 声明类型
      } = {
      x: "Zhang'san",
      y: "abdscdwhv12"
      };

2.2 特殊属性

可选属性
  1. 解释:对象类型中,属性名后加一个问号 "?" 表示可选属性。

    1
    const obj: { name: string, age?: number } = { name: "Jack" };
  2. 使用说明

    • 可选属性的类型等同于属性本身类型 | undefined,即可选属性允许被赋值为 undefined。同时,一个未被赋值的可选属性的值为 undefined。因此,在使用对象的可选属性时,必须检查其是否为 undefined

    • 在使用可选属性前,可以使用三元运算符 "?:"空值合并运算符 "??" 来判断其值是否为 undefined,并设置默认值。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      const user: {
      id: number,
      firstName?: string,
      lastName?: string
      } = {
      id: 12341,
      firstName: "Jack",
      lastName: "Brown"
      }

      const firstName = user.firstName === undefined ? "Tom" : user.firstName;
      const lastName = user.lastName ?? "Wood";

      空值合并运算符 ??:左侧操作数为 nullundefined 时,返回其右侧的操作数,否则返回左侧的操作数。

    • 如果同时使用编译选项 ExcatOptionsPropertyTypesstrictNullChecks,TypeScript 将不允许可选属性设置为 undefined

      1
      2
      3
      4
      5
      6
      7
      const student: {
      name: string,
      age?: number
      } = {
      name: "Jack",
      age: undefined // 报错,在打开 ExcatOptionsPropertyTypes 和 strictNullChecks 编译选项时,可选属性不能赋值为 undefined
      }
只读属性
  1. 解释:对象类型中,属性名前面加上 readonly 关键字,表示只读属性,其只能在对象初始化期间赋值,之后不能被再修改。

    1
    2
    const obj: { name: string, readonly age: number } = { name: "Jack", age: 12 };
    obj.age = 13; // ×
  2. 使用说明

    • 如果只读的属性是一个对象,TypeScript 不会禁止对该对象的属性进行修改,但会禁止对该对象进行替换。

      1
      2
      3
      const stu: Student = { score: { chinese: 80, math: 80, english: 80, average: 80 } };
      stu.score.average = 90; // ✓,允许修改只读的对象属性的属性
      stu.score = { chinese: 80, math: 80, english: 80, average: 80 }; // ×,不允许替换只读的对象属性
    • 如果两个变量(引用)指向同一个对象,且一个变量规定对象属性可写,另一个变量规定对象属性只读,此时,前者对对象属性的修改会影响到后者对对象属性的访问。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      interface WritablePerson {
      name: string;
      age: number;
      }

      interface ReadonlyPerson {
      readonly name: string;
      readonly age: number;
      }

      let w: WritablePerson = { name: "Jack", age: 12 };
      let r: ReadonlyPerson = w;

      w.age++;
      console.log(r.age); // 13
    • 为了让对象的所有属性都是只读的(深度只读),在将对象赋值给变量时可以使用 as const。但要注意,如果对变量添加了类型声明,TypeScript 会优先采用声明的类型,导致 as const 失效。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      /* 使用只读断言的对象是深度只读的 */
      const obj1 = {
      name: "Jack", address: {
      province: "He'nan",
      city: "Xin'xiang",
      detail: {
      district: "Xin'cheng",
      street: 2001,
      }
      }
      } as const;

      obj1.name = "Tom"; // ×
      obj1.address.city = "Xin'yang"; // ×
      obj1.address.detail.street = 9011; // ×

      /* 类型声明会和只读断言发生冲突,以类型声明为准,只读断言失效 */
      const obj2: {
      name: string,
      address: {
      province: string,
      city: string,
      detail: {
      district: string,
      street: number
      }
      }
      } = {
      name: "Jack", address: {
      province: "He'nan",
      city: "Xin'xiang",
      detail: {
      district: "Xin'cheng",
      street: 2001,
      }
      }
      } as const;

      obj2.name = "Tom"; // √
      obj2.address.city = "Xin'yang"; // √
      obj2.address.detail.street = 9011; // √
索引签名
  1. 解释:允许对象拥有任意数量的属性,同时使这些属性满足特定的键值约束,即索引签名。

  2. 语法:{ [indexName: indexType]: valueType }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* 使用索引签名时还可以起一个类型别名(type),或将其提炼为一个接口(interface) */
    type T1 = { [property: string]: string };
    type T2 = { [property: number]: string };
    type T3 = { [property: symbol]: string };

    const t1: T1 = { name: "ZhangSan", gender: "Male" };
    const t2: T2 = { 0: "Hello", 1: "World" };
    const [first, second] = [Symbol(), Symbol()];
    const t3: T3 = { [first]: "Happy", [second]: "New Year" };

    补充:indexName 表示属性名,可以随便起;indexType 是属性名的类型,可以是 stringnumbersymbolvalueType 是属性值的类型。

  3. 使用说明

    • 使用索引签名时,可以同时存在多种类型(属性名的类型不同)的索引。如果同时存在字符串索引和数值索引,必须以字符串索引的属性值类型为准,否则会引发冲突,这是因为 JavaScript 中会自动将数值属性名转换为字符串属性名。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      interface T1 {
      [property: string]: string;
      [property: number]: string;
      } // √

      interface T2 {
      [property: string]: string;
      [property: number]: boolean; // 需要为 string
      } // ×

      interface T3 {
      [property: string]: string;
      [property: symbol]: number
      } // √

      补充:这里的字符串索引和数值索引,指的是索引签名中属性名的类型,即 indexType

    • 同时使用索引签名和特定属性的类型声明时,如果该属性与存在的索引的属性名的类型相同,则属性值的类型一定也要相同,否则会引发冲突。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      interface T1 {
      name: string; // 需为 number;name 属性的属性名类型为 string,其属性值类型与字符串索引的属性值类型产生冲突,从而报错。
      [detail: string]: number;
      }; // ×,

      interface T2 {
      name: string;
      [detail: string]: string;
      }; // √

      interface T3 {
      0: string; // 需为 number;属性名类型为 number 的属性也会和字符串索引产生冲突!
      [detail: string]: number;
      }; // ×

      const x = Symbol();
      interface T4 {
      [x]: string;
      [detail: string]: number;
      };

      interface T5 {
      name: string; // 属性名类型为 string 的属性不会和数值串索引产生冲突!(但是运行时可能会暴露问题,因此不推荐!)
      [detail: number]: number;
      }; // √
    • 可以使用索引签名定义一个表示没有任何属性的对象的类型,代码如下。

      1
      2
      3
      interface WithoutProperties {
      [key: string | number | symbol]: never;
      };

2.3 结构类型

  1. 结构类型原则(structural typing):在 TypeScript 中,若对象 B 具备对象 A 的结构特征,即认为对象 B 的类型兼容对象 A 的类型,这就是结构类型原则。若对象 B 的类型兼容对象 A 的类型,则使用对象A 的地方皆可使用对象 B,即对象 B 可赋值给对象 A 所对应的变量。

    1
    2
    3
    4
    5
    6
    /* Student 类型的对象兼容 Person 类型的对象 */
    interface Person { name: string; age: number; };
    interface Student { name: string; age: number; grade: number; };

    const s = { name: "Jack", age: 19, grade: 9 };
    const p = s;

    补充-1:TypeScript 根据结构类型原则检查某个值是否符合指定类型。

    补充-2:如果类型 B 兼容类型 A,那么类型 B 就是类型 A 的子类型,类型 A 是类型 B 的父类型,即子类型兼容父类型。子类型具有父类型的所有结构特征,同时还具有自己的特征。凡是可以使用父类型的地方,都可以使用子类型。

  2. 严格字面量检查(strict object literal):将对象字面量赋给声明了对象类型的变量时,二者类型必须完全一致,否则会触发严格字面量检查报错。

    1
    2
    3
    4
    5
    6
    7
    8
    const p: {
    name: string,
    age: number,
    } = {
    name: "Jack",
    age: 19,
    height: 190, // ×
    }
  3. 规避严格字面量检查的方式

    • 方式一:使用中间变量(要确保类型兼容!)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      const temp = {
      name: "Jack",
      age: 19,
      height: 190,
      }

      const p: {
      name: string,
      age: number,
      } = temp; // √,temp 兼容 p,也就是说 temp 是 p 的子类型,p 是 temp 的父类型。
    • 方式二:使用类型断言(要确保类型兼容!)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      type Person = {
      name: string,
      age: number,
      }

      const p: Person = {
      name: "Jack",
      age: 19,
      height: 190,
      } as Person; // √
    • 方式三:使用索引签名(要确保类型兼容!)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      type Person = {
      name: string,
      age: number,
      [property: string]: any
      }

      const p: Person = {
      name: "Jack",
      age: 19,
      height: 190,
      }; // √
    • 方式四:打开 suppressExcessPropertyErrors 编译选项,以关闭多余属性检查(要确保类型兼容,且只适用于函数传参!)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      type Person = {
      name: string,
      age: number,
      }

      function logInfo(p: Person) {
      console.log(`info: ${p.name}-${p.age}`);
      }

      logInfo({
      name: "Jack",
      age: 19,
      height: 190,
      }); // √

      补充:Option ‘suppressExcessPropertyErrors’ is deprecated and will stop functioning in TypeScript 5.5(编译选项 suppressExcessPropertyErrors 在 TypeScript 5.5 中被废除)。

2.4 最小可选属性

  1. 解释:如果某个对象类型的所有属性都是可选的,那么该类型的对象必须至少存在一个可选属性,即最小可选属性原则,又称弱类型检测(weak type detection)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    type Coordinate = {
    x?: number;
    y?: number;
    z?: number;
    };

    const temp1 = { d: 100 };
    const cor1: Coordinate = temp1; // ×

    const temp2 = { x: 100, d: 100 };
    const cor2: Coordinate = temp2; // √

    const temp3 = {};
    const cor3: Coordinate = temp3; // √

    补充 - 1:最小可选属性原则是对结构类型原则的补充,否则上述示例中的 cor1 变量不应该报错。

    补充 - 2:当某一类型所对应的对象,既 ① 未涵盖该类型下的所有可选属性,又 ② 存在超出其类型声明范围的其他属性,同时还需 ③ 满足结构类型原则,也就是类型兼容的要求时,此原则才会生效。

  2. 规避方式

    • 方式一:使用索引签名(要确保类型兼容!)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      type Coordinate = {
      x?: number;
      y?: number;
      z?: number;
      [property: string]: any
      };

      const temp = { d: 100 };
      const cor: Coordinate = temp;
    • 方式二:使用类型断言(要确保类型兼容!)

      1
      2
      3
      4
      5
      6
      7
      8
      type Coordinate = {
      x?: number;
      y?: number;
      z?: number;
      };

      const temp = { d: 100 };
      const cor: Coordinate = temp as Coordinate;

TS - 类型系统 4(interface、类)

1. interface

1.1 基本使用

  1. 解释:interface对象的模板,用于指定对象的类型结构,译为接口。接口的使用又称为接口的实现,即将接口当作对象类型来使用。TypeScript 中通过 interface 关键字来定义接口。

    1
    2
    3
    4
    5
    6
    interface IStudent {
    name: string;
    age: number;
    }

    const jack: IStudent = { name: "Jack", age: 19 };
  2. 语法:interface 接口名 = { 接口成员 }

    补充:TypeScript 中使用 interface 关键字定义接口,同时接口成员有以下五种形式:① 对象属性对象的属性索引对象方法函数构造函数

    • 对象属性

      1
      2
      3
      4
      5
      6
      7
      8
      /* 接口成员 - 对象属性
      - 属性之间使用分号 ";" 或逗号 "," 分隔,最后一个属性结尾的分号或逗号可以省略
      - 允许定义可选属性、只读属性 */
      interface A {
      name: string;
      age?: number; // 可选属性
      readonly isFullTime: boolean; // 只读属性
      }
    • 对象的属性索引

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      /* 接口成员 - 对象的属性索引
      - 对象的属性索引的一般格式为 [indexName: indexType]: valueType,其中 indexType 表示属性名类型,可选 string、number、symbol
      - 属性的数值索引,即 indexType 为 number,表示指定数组的类型,如接口 B 所示
      - 接口中同时定义了属性的字符串索引和数值索引,则数值索引必须服从于字符串索引,如接口 C、D 所示。这是因为 JavaScript 中,数值属性名最终会自动转换为字符串属性名 */
      interface B {
      [key: number]: string;
      }
      const arr: B = ['a', 'b', 'c'];

      interface C {
      [key: number]: string;
      [key: string]: string;
      } // √

      interface D {
      [key: number]: number; // 这里应该修改为 string
      [key: string]: string;
      } // ×
    • 对象方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      /* 接口成员 - 对象方法
      - 对象方法一共有三种写法,如接口 E、F、G 所示
      - 对象方法名可以采用表达式,如接口 H 所示
      - 对象方法可以重载,interface 中定义的重载的函数只需要给出重载声明,而不需要给出重载实现,但在使用该接口时,需要给出对应重载的函数的重载声明和重载实现,如 I 所示 */
      interface E {
      f(x: string): string
      };
      interface F {
      f: (x: string) => string;
      };
      interface G {
      f: { (x: boolean): string }
      };

      const f = 'f';
      interface H {
      [f](x: string): string;
      };

      /* 只给出重载声明 */
      interface I {
      f(x: number, y: number): number;
      f(x: string, y: string): string;
      f(x: boolean, y: boolean): boolean;
      };
      /* 使用接口 I 时,定义的对象方法要给出重载声明 + 重载实现 */
      function add(x: number, y: number): number;
      function add(x: string, y: string): string;
      function add(x: boolean, y: boolean): boolean;
      function add(x: number | string | boolean, y: number | string | boolean): number | string | boolean {
      if (typeof x === 'number' && typeof y === 'number') return x + y;
      else if (typeof x === 'string' && typeof y === 'string') return x + y;
      else if (typeof x === 'boolean' && typeof y === 'boolean') return x || y;
      else throw new Error('worng parameters');
      }
      const obj: I = {
      f: add
      }
    • 函数

      1
      2
      3
      4
      5
      6
      /* 接口成员 - 函数
      - 使用接口表示函数的语法如接口 J 所示 */
      interface J {
      (x: string): string;
      }
      const speak: J = (x) => { return x.toUpperCase() };
    • 构造函数

      1
      2
      3
      4
      5
      6
      /* 接口成员 - 构造函数
      - 使用接口表示构造函数的语法如接口 K 所示
      - 在 TypeScript 中,构造函数特指具有 constructor 属性的类 */
      interface K {
      new(name: string, age: number): { name: string, age: number };
      }
  3. 使用说明

    • 与对象类似,TypeScript 允许通过属性/方法名读取对应属性/方法的类型,也就是对 interface 进行索引访问。

      1
      2
      3
      4
      5
      6
      interface IStudent {
      name: string;
      age: number;
      }

      type Name = IStudent['name']; // string

1.2 接口继承

  1. 解释:接口可以使用 extends 关键字继承接口、对象类型、类身上的属性,从而避免书写重复的属性。

  2. interface 继承 interface

    1
    2
    3
    4
    interface A { x: string; }
    interface B extends A { y: string; }

    const obj: B = { x: "hello", y: "world" };
    • 接口允许多重继承,即允许有多个父接口

      1
      2
      3
      4
      5
      interface A { x: string; }
      interface B { y: string; }
      interface C extends A, B { z: string }

      const obj: C = { x: "hello", y: "world", z: "!" };
    • 子接口与父接口的同名属性必须类型兼容,多个父接口之间的同名属性必须类型一致,否则会报错

      1
      2
      3
      4
      5
      6
      7
      8
      9
      /* 父子接口的同名属性需兼容 */
      interface A { x: string | number; }
      interface B extends A { x: string; } // √
      interface C extends A { x: boolean; } // ×

      /* 父接口之间的同名属性需一致 */
      interface M { x: string; }
      interface N { x: number; }
      interface P extends M, N { }; // ×
  3. interface 继承 type

    1
    2
    3
    4
    type A = { x: string; y: string; }
    interface B extends A { z: string; }

    const obj: B = { x: "Hello", y: "World", z: "!" }

    补充:interface 继承 type 的前提是,type 关键字定义的类型是对象

  4. interface 继承 class

    1
    2
    3
    4
    5
    6
    7
    class A {
    x: string = '';
    y(): boolean { return true; }
    }
    interface B extends A { z: number; }

    const obj: B = { x: "Hello", y() { return true }, z: 1 };

    补充:当 class 定义的类中包含 privateprotected 的成员时,interface 继承 class 的意义不大!

1.3 接口合并

  1. 解释:在 TypeScript 中,同名接口会自动合并为一个接口。这一特性旨在兼容 JavaScript 中向全局对象或外部库扩展属性和方法的常见做法。通过定义同名接口,TypeScript 会自动将新增的属性和方法与原始接口合并,从而实现类型的无缝扩展

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* 接口合并 */
    interface A { x: string; }
    interface A { y: string; }

    const obj: A = { x: "Hello", y: "World" };

    /* 类型扩展,以下代码执行后,允许在 document 对象上添加自定义属性 x */
    interface Document { x: string; }
    document.x = "Hello";
  2. 使用说明

    • 接口合并时,同名属性必须类型一致,否则会报错

      1
      2
      interface A { x: string; }
      interface A { x: number; } // ×
    • 接口合并时,同名方法可以有不同的类型声明,即方法重载,此时:后边定义的接口的方法声明优先级更高;包含字面量参数的方法声明优先级最高。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      interface B {
      add(x: "hello"): void;
      }
      interface B {
      add(x: boolean): boolean;
      }
      interface B {
      add(x: boolean | number): string;
      add(x: number): number;
      }
      // 上述代码等价于
      interface B {
      add(x: "hello"): void;
      add(x: boolean | number): string;
      add(x: number): number;
      add(x: boolean): boolean;
      }
    • 两个 interface 的联合类型中的同名属性也是联合类型

      1
      2
      3
      interface C { x: string; }
      interface D { x: number };
      type E = C | D; // string | number

1.4 interface Vs. type

  1. 相同点:interfacetype 都可以定义对象类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    type Country = {
    name: string;
    capital: string;
    }
    // 等价于
    interface Country {
    name: string;
    capital: string;
    }
  2. 不同点

    概述 interface type
    类型范围 对象类型(包括数组、函数等) 对象类型 + 非对象类型
    继承方式 使用 extends 关键字继承接口、对象类型或类 使用 & 运算符继承接口、对象类型
    合并允许 同名 interface 会自动合并(开放) 同名 type 会报错(封闭)
    属性映射 不能包含属性映射 可以包含属性映射
    this 允许 允许使用 this 不允许使用 this
    原始数据类型扩展 不允许扩展原始数据类型 允许扩展原始数据类型
    复杂类型表示 不可以表示复杂类型 可以表示复杂类型(如交叉类型和联合类型)

    补充:一般情况下,由于 interface 灵活度较高,建议优先使用~

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    /* 属性映射 */
    interface Data {
    x: number;
    y: string;
    }

    type Copy1 = {
    [key in keyof Data]: Data[key];
    } // √,Copy1 是一种基于映射类型的方式来精确复制 Data 的结构。

    interface Copy2 {
    [key in keyof Data]: Data[key];
    } // ×

    /* 原始数据类型扩展 */
    interface MyStr1 extends string {
    type: "new";
    } // ×,An interface cannot extend a primitive type like 'string'. It can only extend other named object types.(2840)

    type MyStr2 = string & {
    type: "new";
    } // √

    const str: MyStr2 = new String("hello") as MyStr2;
    str.type = "new"
    console.log(str, str.type) // String: "hello", "new"

2. 类

2.1 基本使用

  1. 解释:类是面向对象编程的核心构建单元,它通过封装属性(数据)和方法(行为)来实现对现实世界实体的抽象和建模。TypeScript 中通过 class 关键字定义类。

  2. 基本语法:class 类名 { 属性; 方法; /* ... */ }

    补充:TypeScript 中使用 class 关键字定义类,同时类成员有以下五种形式:① 属性只读属性方法访问器方法属性索引

    • 属性:可以在类顶部声明属性及其类型。在未指定属性类型的情况下,如果属性被赋初值,TypeScript 会自动推断该属性类型,否则会认为该属性类型为 any。一般情况下,编译选项 --strictPropertyInitialization 被打开,此时 TypeScript 会检查属性是否设置了初值,没有就报错(如果在构造函数中进行了赋值,也不会报错)。为了避免因为属性未赋值而产生的报错,可以使用非空断言 "!",表示对应的属性肯定不会为空。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      class Point {
      x: number = 10; // 标准写法
      y = 10; // 自动推断属性 y 为 number 类型
      z; // 认为属性 z 为 any 类型
      isValid!: boolean; // 非空断言,表示属性 isValid 一定不会为 null 或 undefined
      isOrigin: boolean; // 因为在构造函数中对 isOrigin 进行了赋值,所以这里不会报错

      constructor(x: number, y: number, z: number, isValid: boolean, isOrigin: boolean) {
      this.x = x;
      this.y = y;
      this.z = z;
      this.isValid = isValid;
      this.isOrigin = isOrigin;
      }
      }

      非空断言(Non-null Assertion):TypeScript 中通过在表达式末尾添加一个感叹号 "!" 实现非空断言,告诉编译器该表达式的值绝对不是 nullundefined

    • 只读属性:属性名前使用 readonly 修饰符以表示该属性是只读属性。可以在类顶部给只读属性赋值,也可以在构造函数中给只读属性赋值,如果两处同时赋值,以构造函数中的为准。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      class Point {
      readonly x: number = 10;
      readonly y: number;
      readonly z: number = 10;

      constructor() {
      this.y = 20;
      this.z = 30;
      /* 此时属性为 x = 10, y = 20, z = 30 */
      }
      }
    • 方法:声明方法的语法为 methodName(paraName1: paraType1, /* ... */) {},其中可以省略返回值类型,让 TypeScript 自行推断。方法的参数与普通函数的参数一样,可以使用参数默认值。同时,方法也可以进行重载(包括构造方法)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      class Point {
      readonly x: number;
      readonly y: number;

      /* 参数默认值 */
      add(point: Point = new Point(1, 1)) {
      return new Point(
      this.x + point.x,
      this.y + point.y
      )
      }

      /* 方法重载 */
      mul(times: number): Point;
      mul(point: Point): Point;
      mul(para: number | Point): Point {
      if (typeof para === 'number') {
      return new Point(
      this.x * para,
      this.y * para
      );
      } else if (para instanceof Point) {
      return new Point(
      this.x * para.x,
      this.y * para.y
      );
      } else {
      throw new Error("Invalid parameter type. Expected number or Point.");
      }
      }

      constructor(x: number, y: number) {
      this.x = x;
      this.y = y;
      }
      }

      补充:构造方法不能声明返回值类型,因为其总是返回实例对象。

    • getter / setter(存取器方法):存取器(accessor)是特殊的方法,包括取值器(getter)和存值器(setter),其中取值器用于读取属性,存值器用于写入属性(这里读取或写入的属性称之为访问器属性)。一般来说,存取器对类中的某个属性进行代理,从而对其读取和写入操作添加更加更加详细的控制。取值器语法为 get 属性名(){ return 被代理属性 },存值器语法为 set 属性名(value: valueType){ 被代理属性 = value }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      class Observer {
      _data: { [key: string]: any } = {};

      get data() {
      return this._data;
      }

      set data(value: { [key: string]: any }) {
      this._data = value
      }
      }

      补充-1:如果一个访问器属性只有 getter,那么该属性就是只读属性。

      补充-2:TypeScript 5.1 之前要求 getter 的返回值型必须兼容 setter 的参数类型。

      补充-3:访问器属性的 gettersetter 的可访问性必须一致!要么都公开,要么都私有!

    • 属性索引:在 TypeScript 中,类允许定义属性索引(如字符串、数字或 Symbol),用于约束对应属性值的类型。方法和存取器被视为特殊的字符串属性索引。例如,[s: string]: boolean | ((s: string) => boolean) 约束了类中所有以字符串为属性名的属性:这些属性的值必须是布尔值,方法必须是返回布尔值的函数,而存取器必须返回布尔值。一旦不满足约束,就会报错。

  3. 静态成员:类的内部能够使用 static 关键字来定义静态成员(包括属性或方法)。静态成员只能通过类本身来使用,并且可以使用 publicprivateprotected 修饰符来修饰静态成员。

2.2 类的实现

  1. 解释:TypeScript 中,可以通过 interfacetype 以对象的形式对类的成员进行约束,类可以使用 implements 关键字应用这些约束,称之为类的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    interface Person {
    name: string;
    age: number;
    }

    class Chinese implements Person {
    name: string;
    age: number;
    city: string = "Bei'jing";

    constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
    }
    }

    补充-1:interface 定义的类成员约束与类自身的类型声明并不等同,所以类在符合 interface 所提供的约束条件时,还需补充相应的类型声明

    补充-2:类可以定义 interface 中没有声明的属性和方法。

    补充-3:如果 implements 关键字后边是一个类,此时这个类被当做接口使用

  2. 多重实现:在 TypeScript 中,类能够实现多个接口,这意味着它可以接受来自多个接口的约束。当类实现多个接口时,接口之间用逗号分隔。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    interface A {
    x: string;
    }

    interface B {
    y: number;
    }

    interface C {
    z: boolean;
    }

    class MyClass implements A, B, C {
    x: string = '1';
    y: number = 1;
    z: boolean = true;
    }

    补充-1:若一个类同时实现多个接口,可能会导致代码管理变得困难,此时可借助类的继承或者接口的继承来对这种情况加以改写。

    补充-2:当一个类实现多个接口时,各接口之间不能存在会引发冲突的属性

  3. 类的合并:TypeScript 中不允许出现两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类的定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    interface Car {
    isNew: boolean;
    }

    class Car {
    price: number = 10000;
    }

    const car = new Car();
    car.isNew = true; // 如果这里不给 isNew 属性赋值,则默认为 undefined

2.3 类的类型

  1. 实例类型:在 TypeScript 里,类(名)本身就属于一种类型,它代表的是该类的实例类型,而非类自身的类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Point {
    x: number;
    y: number;

    constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
    }
    }

    const p1: Point = new Point(1, 2); // 这里 Point 被当做实例类型使用
    const p2: Point = { x: 1, y: 2 }; // 这里 Point 被当做对象类型使用

    补充-1:若类 A 实现了接口 B,在声明引用类 A 实例的变量时,既可将变量类型声明为 A,也能声明为 B。二者唯一的区别在于,当声明类型为 B 时,该变量无法访问类 A 中定义的那些属性和方法。

    补充-2:在 TypeScript 中,存在三种可充当对象类型的方式,分别是 typeinterface 以及 class

  2. 自身类型:可通过以下三种方式获得一个类的自身类型。

    • 方式一 typeof 类名

    • 方式二 new (paraName1: paraType1, paraName2: paraType2) => 类名

      补充:方式二的合理性在于,在 JavaScript 中,类是构造函数的语法糖,即构造函数的另一种写法,因此类的自身类型可以写成构造函数的形式。

    • 方式三 { new (paraName1: paraType1, paraName2: paraType2): 类名 }

      补充:可以参照方式三将类的自身类型提炼为一个 interface,便于使用。

  3. 结构类型

    • 如果一个对象满足类的实例结构,便认为此对象和该类是同一类型。

      1
      2
      3
      4
      5
      6
      7
      class Person {
      name: string = "Jack";
      age: number = 19;
      static isAdult: boolean = true;
      }

      const p: Person = { name: "Tom", age: 41 };

      补充-1:确定对象和类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法。

      补充-2:当对象和类属于同一类型时,不可以使用 对象 instanceof 类名 的方式判断某个对象是否跟某个类属于同一类型。

    • 如果类 A 具备类 B 的全部结构,即便类 A 还有额外的属性和方法,也会认为类 A 是兼容类 B 的。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      class Person {
      name: string = "Jack";
      age: number = 19;
      static isAdult: boolean = true;
      }

      class Student {
      name: string = "Tom";
      age: number = 89;
      grade: number = 7;
      static isAdolescent: boolean = true;
      }

      const stu: Person = new Student();

      补充-1:确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法。

      补充-2:若类中包含私有成员或保护成员,在判定两个类的兼容关系时,需保证这些私有或保护成员源自同一个类,也就是说,这两个类之间得存在继承关系。

2.4 类的继承

  1. 解释:一个类(子类)可以使用 extends 关键字继承另一个类(父类、基类)的所有属性和方法。

    1
    2
    3
    4
    5
    6
    7
    8
    class Creature {
    eat() { console.log("eat food...") }
    }

    class Cat extends Creature { }

    const cat = new Cat();
    cat.eat(); // "eat food..."
  2. 使用说明

    • 依照结构类型原则,因为子类涵盖了父类的全部结构,子类兼容父类

    • 子类能够对父类里的同名方法进行重写,但其类型定义务必要兼容父类中该同名方法的类型定义。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      class Creature {
      eat() { console.log("eat food...") }
      }

      class Cat extends Creature {
      eat(n?: number) {
      if (n === undefined) super.eat();
      else console.log(`eat ${n} fish...`);
      }
      }

      const cat = new Cat();
      cat.eat(12);

      补充-1:当子类重写父类的方法时,若在重写的方法前添加 override 关键字,TypeScript 便会检查父类中是否存在对应的同名方法,若不存在,就会报错。

      补充-2:在 TypeScript 编译过程中,若开启了 noImplicitOverride 编译选项,那么当子类重写父类里的同名方法时,要是不添加 override 关键字,就会出现报错。

    • 在子类中,能够将父类的保护成员的可访问性修改为公开,但不能把它设为私有。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      class A {
      protected x = 1;
      protected y = 2;
      protected z = 3;
      }

      class B extends A {
      public x = 1; // √
      private y = 2; // ×
      protected z = 3; // √
      }
    • extends 关键字后不一定是类名,可以是一个类型为构造函数的表达式。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      class Bike {
      speed = "slow";
      drive() { console.log("Bike rides slowly") }
      }

      class Car {
      speed = "fast";
      drive() { console.log("Car drives quickly") }
      }

      interface Transportation {
      speed: string;
      drive(): void;
      }

      interface TransportationConstructor {
      new(): Transportation;
      }

      function getTransportation(): TransportationConstructor {
      return Math.random() > 0.5 ? Bike : Car;
      }

      /* getTransportation() 是一个表达式,执行返回 Bike 或 Car */
      class Vehicle extends getTransportation() {
      sayInfo() {
      console.log(`The speed is ${this.speed}`);
      this.drive();
      }
      }

      const v = new Vehicle();
      v.sayInfo();

2.5 可访问性修饰符

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

可访问性修饰符 解释 外部(实例)可访问 类内部可访问 子类可访问
public 公开成员(默认修饰符)
private 私有成员 × ×
protected 保护成员 ×
  1. private

    • 子类中不能定义父类私有成员的同名成员

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      class Creature {
      private name: string = "creature";
      private age: number = 0;
      private gender: string = "male";
      }

      class Dog extends Creature {
      public name: string = "dog"; // ×
      protected age: number = 0; // ×
      private gender: string = "male"; // ×
      }
    • private 修饰符定义的私有成员并不是真正意义上的私有成员,实例对象可以通过方括号 "[]"in 运算符访问对应的私有成员。因此,更推荐使用 ES2022 引入的私有成员写法 #propName,即真正意义上的私有成员

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      class Creature {
      private name: string = "creature";
      #age: number = 0;
      }

      const c = new Creature();

      c.name; // ×
      c['name']; // "creature"
      'name' in c; // true

      c.age; // ×
      c['age']; // ×
      'age' in c; // false
    • 构造函数设为私有后,不能用 new 操作符生成实例对象,只能在类内创建。类内创建实例的静态方法工厂函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      class Creature {
      name: string;
      age: number;

      private static instance?: Creature; // Creature 类的唯一实例

      private constructor(name: string, age: number) {
      this.name = name;
      this.age = age;
      }

      /* 工厂函数,用于获取 Creature 类实例(单例) */
      static getInstance() {
      if (!Creature.instance)
      Creature.instance = new Creature("lucky", 12);
      return Creature.instance;

      }
      }
  2. protected

    • 子类中可以定义父类保护成员的同名成员

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      class Creature {
      protected name: string = "creature";
      protected age: number = 0;
      protected gender: string = "male";
      }

      class Dog extends Creature {
      public name: string = "dog"; // √
      protected age: number = 0; // √
      private gender: string = "male"; // ×
      }
  3. 实例属性的简写形式:在 TypeScript 中,支持将以下代码 (1) 简写为代码 (2),即可访问性修饰符 publicprivateprotected 以及只读修饰符 readonly 移动到构造函数中去。

    补充:readonly 可以和 publicprivateprotected 混合使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // (1)
    class Point {
    x: number;
    y: number;

    constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
    }
    }
    1
    2
    3
    4
    // (2)
    class Point {
    constructor(public x: number, public y: number) { }
    }

2.6 顶层属性的初始化问题

  1. 背景:对于类的顶层属性,在早期 TypeScript 中,① 在类顶层声明属性,② 运行构造方法,同时完成初始化;在 ES2022 及以后,① 在类顶层声明属性,同时完成初始化,② 运行构造方法。因此,同一段代码在 TypeScript 和 JavaScript 中的运行结果可能会有所不同。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // TypeScript 顶层属性在构造方法中初始化
    class Point {
    x: number = 1;
    y: number = 2;

    constructor(x: number, y: number) {
    // 这里隐含了代码
    // this.x = 1;
    // this.y = 2;
    console.log(this.x, this.y);
    this.x = x;
    this.y = y;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // JavaScript 顶层属性在类顶层初始化
    class Point {
    x = 1;
    y = 2;

    constructor(x, y) {
    console.log(this.x, this.y);
    this.x = x;
    this.y = y;
    }
    }
  2. 问题:因为顶层属性初始化位置不一致而导致代码运行结果不一致的情况通常发生在以下两种情形。

    • 顶层属性的初始化依赖于其他实例属性

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // TypeScript
      class User {
      age = this.currentYear - 1998;

      constructor(private currentYear: number) {
      // 这里隐含了
      // this.currentYear = currentYear;
      // this.age = this.currentYear - 1998;
      console.log('Current age:', this.age);
      }
      }

      const user = new User(2023); // 25
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // JavaScript
      class User {
      age = this.currentYear - 1998; // undefined - 1998 = NaN

      constructor(currentYear) {
      console.log('Current age:', this.age);
      }
      }

      const user = new User(2023); // NaN
    • 子类声明的顶层属性在父类完成初始化

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      // TypeScript
      interface Animal {
      animalStuff: any;
      }
      interface Dog extends Animal {
      dogStuff: any;
      }
      class AnimalHouse {
      resident: Animal;

      constructor(animal: Animal) {
      // 这里隐含了
      // this.resident = undefined
      this.resident = animal;
      }
      }
      class DogHouse extends AnimalHouse {
      resident: Dog;

      constructor(dog: Dog) {
      // 这里隐含了
      // this.resident = undefined
      super(dog);
      }
      }

      const dog = {
      animalStuff: 'animal',
      dogStuff: 'dog'
      };
      const dogHouse = new DogHouse(dog);
      console.log(dogHouse.resident);
      /*
      {
      "animalStuff": "animal",
      "dogStuff": "dog"
      }
      */
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      // JavaScript
      class AnimalHouse {
      resident;
      constructor(animal) {
      this.resident = animal;
      }
      }
      class DogHouse extends AnimalHouse {
      resident;
      constructor(dog) {
      super(dog);
      }
      }

      const dog = {
      animalStuff: 'animal',
      dogStuff: 'dog'
      };
      const dogHouse = new DogHouse(dog);
      console.log(dogHouse.resident); // undefined
  3. 解决方式

    • 使用 TypeScript 3.7 开始提供的编译选项 useDefineForClassFields,该编译选项为 true,则表示采用 ES2022 标准的处理方法,否则采用 TypeScript 早期的处理方法。编译选项 useDefineForClassFields 的默认值与编译选项 target 有关,如果 target 的取值为 ES2022 或更高,那么 useDefineForClassFields 的取值为 true,否则为 false
    • 将所有顶层属性的初始化都放到构造方法中
    • 对于类的继承导致的问题,可以使用 declare 命令,去声明子类的顶层属性的初始化由父类实现

2.7 抽象类

抽象类:一种特殊的类,其类名前需加上 abstract 关键字。抽象类是其他类的模板,用于定义一些共有的接口,不能进行实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
abstract class Point {
/* 非抽象成员 */
name: string = "Point";
logDescription() { console.log("Point Abstract Class") };
/* 抽象成员 */
abstract x: number;
abstract y: number;
abstract isOrigin(x: number, y: number): boolean;
}

class RealPoint extends Point {
constructor(public x: number, public y: number) {
super();
}
isOrigin(x: number, y: number) { return x === 0 && y == 0 }
}
  • 抽象类包括抽象成员和非抽象成员,抽象成员是未实现的属性和方法,使用 abstract 关键字修饰,需要在非抽象子类中实现;非抽象成员是已实现好的属性和方法。
  • 抽象类的子类也可以是抽象类。
  • 抽象成员不能被 private 修饰符修饰。

2.8 this 之用

类的方法中使用的 this 表示该方法当前所在的对象

1
2
3
4
5
6
7
8
9
10
11
class A {
constructor(public name: string) { }

getName() { return this.name; }
}

const a1 = new A("hello");
const a2 = { name: "world", getName: a1.getName };

console.log(a1.getName()); // "hello",getName() 中的 this 指向 a1
console.log(a2.getName()); // "world",getName() 中的 this 指向 a2
  • TypeScript 允许函数增添一个名为 this 的参数,位于参数列表的首位,用于描述函数内部 this 的类型。在编译时,TypeScript 会对函数进行检查,如果函数参数列表的第一个参数名为 this,那么编译的结果将会删除这个参数。

  • TypeScript 中提供编译选项 noImplicitThis,打开这个编译选项后,如果 this 的值被推断为 any 类型就会报错。

  • 在类内部,this 可以当作类型(返回值)来使用,表示当前类的实例对象。如果某个方法的返回值是一个布尔值,用于表明 this 是否属于某种类型,此时可以将 this is Type 作为方法的返回值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class MyArray<T> {
    constructor(public arr: T[]) { }

    reverse(): this {
    this.arr.reverse();
    return this;
    }

    slice(a: number, b: number): this {
    this.arr = this.arr.slice(a, b);
    return this;
    }

    isMyArray(): this is MyArray<T> {
    return this instanceof MyArray;
    }
    }

    const arr = new MyArray([1, 2, 3, 4, 5]);
    console.log(arr.reverse());
    console.log(arr.slice(0, 2));
    console.log(arr.isMyArray());

TS - 类型系统 5(泛型、Enum)

1. 泛型

1.1 基本使用

  1. 解释:泛型(Generics)是一种特殊的编程逻辑,它通过引入类型参数(type parameter)来建立输入类型和输出类型之间的关联。在 TypeScript 中,泛型应用于函数、接口、类和类型别名中,类型参数放置在尖括号 <> 中。类型参数必须是合法的标识符,常用 T、U、V 等字母命名,各个类型参数之间使用逗号 , 分隔。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function getRandomElement<T>(arr: T[]): T {
    const randomIndex = Math.floor(Math.random() * arr.length);
    return arr[randomIndex];
    }

    console.log(getRandomElement<string>(['a', 'b', 'c']));
    console.log(getRandomElement(['a', 'b', 'c'])); // 有时可以省略类型参数的值,让 TypeScript 自行推断


    function combine<T>(arr1: T[], arr2: T[]): T[] {
    return arr1.concat(arr2);
    }

    console.log(combine<number | string>([1, 2, 3], ['a', 'b', 'c'])); // 有时类型过于复杂,TypeScript 无法推断,此时需要显示给出类型参数的值


    function map<T, U>(arr: T[], f: (arg: T) => U): U[] {
    return arr.map(f);
    }

    console.log(map<string, number>(['1', '2', '3'], (x) => parseInt(x))); // 可使用多个类型参数
  2. 语法

    • 函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // 写法一:函数声明
      function func1<T>(msg: T): T {
      return msg;
      }

      // 写法二:函数类型 - 一般写法
      let func2: <T>(arg: T) => T = func1;

      // 写法三:类型声明 - 对象写法
      let func3: { <T>(arg:T): T } = func1;
    • 接口

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      // 情形一:接口定义属性类型
      interface Info<T> {
      contents: T;
      }

      // 情形二:接口定义方法或函数类型
      // 2-1:类型参数定义在接口,实现该接口时需要给出类型参数的值。
      interface Comparator<T> {
      compareTo(value: T): number;
      }
      // 2-2:类型参数定义在方法或函数中,实现该接口时不需要给出类型参数的值,使用对应方法或函数时才需要给出类型参数的值。
      interface Fn {
      <T>(arg: T): T;
      }
    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      // 写法一:一般写法
      class Pair<K, V> {
      key: K;
      value: V;
      }

      // 写法二:类表达式
      // 补充:类表达式包含匿名类表达式和具名类表达式,后者更有利于调试,这里使用了匿名类表达式作为示例
      const Person = class <T> {
      constructor(public name: string, public age: number, public info: T) { }
      }

      // 写法三:构造函数类型写法
      // 解释:JavaScript 中的类本质上是一个构造函数
      type MyClass1<T> = new (...args: any[]) => T;
      interface Myclass2<T> {
      new(...args: any[]): T;
      }

      /* 注意事项
      - 泛型类描述的是类的实例,因此类型参数不可应用于类中的静态属性和静态方法
      */
    • 类型别名

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // 示例一:定义联合类型
      type Nullable<T> = T | undefined | null;

      // 示例二:定义对象类型
      type Container<T> = { value: T };

      // 示例三:定义二叉树类型
      type Tree<T> = {
      value: T;
      left: Tree<T> | null;
      right: Tree<T> | null;
      }
  3. 使用说明

    • 类型参数至少需要出现两次,否则可能是非必要的。

    • 泛型允许嵌套,即类型参数可以是另一个泛型。

      1
      2
      3
      type OrNull<T> = T | null;
      type OneOrMany<T> = T | T[];
      type OneOrManyOrNull<T> = OrNull<OneOrMany<T>>; // 等价于 T | T[] | null

1.2 可选的类型参数

解释:在TypeScript中,类型参数可以设置默认值。

  • 当使用泛型但未指定类型参数时,将使用这个默认值。然而,如果 TypeScript 能够推断出类型参数的值,那么推断出的值将覆盖默认值。
  • 类型参数的默认值在中非常常见。
  • 设置了默认值的类型参数也被称为可选类型参数。在多个类型参数列表中,可选类型参数必须位于最后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function getFirst<T = string>(arr: T[]): T {
return arr[0];
}

console.log(getFirst([1, 2, 3])); // 此时类型参数 T 被 TypeScript 推断为 number,覆盖了 T 的默认值 string


class Stack<T = string> {
list: T[] = [];

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

const s1 = new Stack();
s1.add(1); // ❌
s1.add('1'); // ✅


type Color<T, V = 'yellow'> = T | V | "red" | "green" | "blue"; // ✅
type Direction<T = 'up', V> = T | V | 'down'; // ❌

1.3 类型参数的约束条件

解释:在 TypeScript 中,可以为类型参数设置约束条件,语法为 <TypeParameter extends ConstraintType>。这表示类型参数TypeParameter 必须是约束类型 ConstraintType 的子类型。

  • 同时,类型参数可以设置默认值和约束条件,但默认值必须满足约束条件。
  • 也可以使用其他类型参数作为约束条件,如 <T, U extends T>。然而,类型参数不能以自身作为约束条件,如 <T extends T> 是不允许的。
1
2
3
4
5
6
7
8
9
10
11
12
function comp<T extends { length: number }>(a: T, b: T) { // 这里约束类型参数 T 必须包含 number 类型的属性 length
if (a.length >= b.length) return a;
else return b;
}

console.log(comp([1, 2, 3], ['1', '2']));
console.log(comp('123', 'ABCD'));


type tupleType<T extends string, U extends string = 'world'> = [T, U];

const t: tupleType<'hello'> = ['hello', 'world'];

2. Enum

1.1 基本使用

  1. 解释:Enum 是 TypeScript 新增的一种数据结构和类型,译为枚举,用于集中管理一组有关系的常量,从而增加代码的可读性和可维护性。枚举通过 enum 关键字定义,分为数值枚举const 枚举字符串枚举

    补充-1:可以通过点运算符或者方括号运算符来访问枚举中的某个成员。

    补充-2:枚举既是一种类型,也是一个。编译之后的枚举是一个 JavaScript 对象。

    补充-3:由于枚举编译后是一个对象,因此建议谨慎使用枚举。一般来说,枚举可以被对象的 as const 断言所取代。同时,不能存在与枚举同名的变量(包括对象、函数、类等)。

  2. 数值 Enum:数值 Enum 即枚举成员的值为数值的枚举,如果枚举成员未赋值,每个成员的值默认从 0 开始有序递增,如 0、1、2、……

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    enum Direction {
    Up, // 0
    Down, // 1
    Left, // 2
    Right // 3,枚举成员的值默认是数值,且从 0 开始有序递增
    }

    console.log(Direction.Up); // 0,点运算符访问枚举成员
    console.log(Direction['Down']); // 1,方括号运算符访问枚举成员

    const up1: number = Direction.Up;
    const up2: Direction = Direction.Up; // 枚举不仅仅是一个值,也是一种类型

    const Direction = 1; // ×,不能有与枚举同名的变量

    补充-1:数值 Enum 支持为成员显式赋数值,但不能是大整数。其中,成员的值可以相同。

    补充-2:数值 Enum 中,如果只设定某一个成员的值,后续成员的值就会以这个值为基准有序递增,如 N、N + 1、N + 2、……

    补充-3:数值 Enum 中成员的值可以是表达式

    补充-4:数值 Enum 中成员的值都是只读的,无法重新赋值。

  3. const Enum:const Enum 即特殊的 Enum,其与一般 Enum 的区别在于 ① const Enum 使用 const enum 进行定义;② TypeScript 不会将 const Enum 编译为一个对象,而是会将所有 Enum 成员出现的场合替换为对应成员的值,有利于提高性能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const enum Direction {
    Up, // 0
    Down, // 1
    Left, // 2
    Right // 3
    }

    Direction.Up; // 被编译为 0 /* Direction.Up */;
    Direction.Down; // 被编译为 1 /* Direction.Down */;
    Direction.Left; // 被编译为 2 /* Direction.Left */;
    Direction.Right; // 被编译为 3 /* Direction.Right */;
    Direction; // ×,因为 Direction 没有编译产物,TypeScript 只会把代码中 Direction 成员替换为对应的值

    补充:如果希望 const Enum 编译产物仍为对象,那么就要打开编译选项 preserveConstEnum

  4. 字符串 Enum:字符串 Enum 即枚举成员的值为字符串的枚举,与数值 Enum 不同,字符串 Enum 成员值必须显式设置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    enum Direction {
    Up, // 0
    Down, // 1
    Left = "Left",
    Right = "Right"
    }

    let d1 = Direction.Up;
    let d2 = Direction.Left;
    d1 = 1; // 可以赋值数值,但是只能赋值 0 或 1(枚举中已有的成员的值)
    d2 = "Left"; // 不可以赋值字符串,即使赋值 "Left" 或 "Right"(枚举中已有的成员的值)

    let d3: Direction = Direction.Left;
    let d4: "Left" | "Right" = d3; // 字符串 Enum 可以使用联合类型替代

    补充-1:字符串 Enum 允许成员值包含数值和字符串,但是在字符串 Enum 中,如果设置了某个成员值为字符串,那么其后的成员都需要设置为字符串,其前的成员的值默认从 0 开始有序递增

    补充-2:Enum 的成员值只能是数值和字符串

    补充-3:如果一个变量类型是数值 Enum,那么还可以将其他数值赋值给该变量;如果一个变量类型是字符串 Enum,那么不能将其他字符串赋值给该变量

    补充-4:字符串 Enum 做类型用时,可以使用联合类型来替代。

    补充-5:字符串 Enum 成员值不能使用表达式进行赋值。

1.2 Enum 合并

  1. 解释:多个同名的 Enum 会自动合并。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    enum Color {
    red = 'red',
    green = 'green'
    }

    enum Color {
    yellow = 'yellow',
    blue = 'blue'
    }

    /* 等价于 */

    enum Color {
    red = 'red',
    green = 'green',
    yellow = 'yellow',
    blue = 'blue'
    }
  2. 使用说明

    • 多个 Enum 合并时,只允许其中一个的首个成员省略显式赋值,否则会报错。
    • 多个 Enum 合并时,不允许有同名成员,否则报错。
    • 多个 Enum 合并时,要么都是 const Enum,要么都是非 const Enum。

1.3 反向映射

解释:反向映射指的是能通过成员值获取成员名,不过只有数值 Enum 才有反向映射,这是因为 TypeScript 对不同Enum的编译结果不一样造成的。

  • 数值 Enum 的编译结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    enum Direction {
    Up,
    Down,
    Left,
    Right
    }

    /* 上述代码的编译结果为, */
    /*

    "use strict";
    var Direction;
    (function (Direction) {
    Direction[Direction["Up"] = 0] = "Up";
    Direction[Direction["Down"] = 1] = "Down";
    Direction[Direction["Left"] = 2] = "Left";
    Direction[Direction["Right"] = 3] = "Right";
    })(Direction || (Direction = {}));

    */

    /* 上述编译结果等价于, */
    /*

    {
    "0": "Up",
    "1": "Down",
    "2": "Left",
    "3": "Right",
    "Up": 0,
    "Down": 1,
    "Left": 2,
    "Right": 3
    }

    */
  • 字符串 Enum 的编译结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    enum Direction {
    Up = "Up",
    Down = "Down",
    Left = "Left",
    Right = "Right"
    }

    /* 上述代码的编译结果为, */
    /*

    "use strict";
    var Direction;
    (function (Direction) {
    Direction["Up"] = "Up";
    Direction["Down"] = "Down";
    Direction["Left"] = "Left";
    Direction["Right"] = "Right";
    })(Direction || (Direction = {}));

    */

    /* 上述编译结果等价于, */
    /*

    {
    "Up": "Up",
    "Down": "Down",
    "Left": "Left",
    "Right": "Right"
    }

    */

1.4 Enum 提取

  1. 成员名提取:TypeScript 中,可通过 keyof typeof 枚举名 这种方式,提取指定枚举的全部成员名,并将其作为联合类型返回。

    1
    2
    3
    4
    5
    6
    7
    8
    enum Direction {
    Up = "up",
    Down = "down",
    Left = "left",
    Right = "right"
    }

    type D = keyof typeof Direction; // "Up" | "Down" | "Left" | "Right"
  2. 成员值提取:在 TypeScript 中,可通过 in 运算符提取指定枚举的全部成员值

    1
    2
    3
    4
    5
    6
    7
    8
    enum Direction {
    Up = "up",
    Down = "down",
    Left = "left",
    Right = "right"
    }

    type D = { [key in Direction]: any }; // { up: any; down: any; left: any; right: any; }

TS - 类型断言

基本使用

  1. 类型断言:TypeScript 支持类型断言,允许开发者在代码中指定某个值的类型,此时编译器会放弃对该值的类型推断。

  2. 语法

    • <Type>value(不推荐,因为与 TypeScript 支持的 React-JSX 语法冲突)
    • value as Type(推荐)
  3. 使用说明

    • 可以使用类型断言解决对象类型的严格字面量检查报错。

      1
      const p: { x: number } = { x: 0, y: 0 } as { x: number, y: number }; // ✅
    • 可以使用类型断言指定 unknown 类型的变量的具体类型。

      1
      2
      const str1: unknown = 'Hello World';
      const str2: string = str1 as string; // ✅
    • 类型断言必须满足 “实际类型是断言类型的子类型” 或 “断言类型是实际类型的子类型”。但是该条件可以通过 “先将指定值断言为 anyunknown 类型,再将其断言为目标类型” 来避免。

      1
      2
      3
      const n = 1;
      const m:string = n as string; // ❌
      const k:string = n as any as string; // ✅

as const 断言

  1. as const 断言:这种断言只能应用于字面量,它将字面量的类型断言为不可变类型,进一步缩小为 TypeScript 允许的最具体的类型

  2. 语法

    • <const>value
    • value as const
  3. 使用说明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 1. as const 断言将某个值推断为值类型,此时 let 变量不能再改变值
    let s = 'javascript' as const; // 类型为 'javascript'

    // 2. as const 断言不能用于变量和表达式
    let s1 = s as const; // ❌
    let s2 = ('java' + 'script') as const; // ❌

    // 3. as const 断言可以应用于整个对象(只读类型),也可以应用于对象中的单个属性(值类型,不可变)。
    const p1 = { name: "Jack", age: 12 as const }; // 类型为 { name: string, age: 12 }
    const p2 = { name: "Jack", age: 12 } as const; // { readonly name: "Jack", readonly age: 12 }

    // 4. as const 断言应用于数组,将其断言为只读元组。(元组长度可定,数组长度不可定,因此可以使用扩展运算符 + 元组为函数传参)
    const tuple = [1, 2, 3] as const; // 类型为 readonly [1, 2, 3]

    // 5. as const 断言应用于枚举成员,将其断言为枚举的指定成员,此时 let 变量不能再改变为其他枚举成员
    enum Directions { Up, Down, Left, Right }
    let d1 = Directions.Down; // 类型为 Directions
    let d2 = Directions.Down as const; // 类型为 Directions.Down

非空断言

  1. 非空断言:这种断言用于断言某些可能为空(可能为 undefinednull)的变量不会为空,避免编译报错。

    1
    2
    3
    4
    5
    const root = document.getElementById('root');

    root!.addEventListener('click', e => {
    /* ... */
    })
  2. 语法:变量!

  3. 使用说明

    • 非空断言需要开发者确保一个表达式的值不为空。更保险的方式是,使用可能为空的变量前先进行手动检查。
    • 非空断言只有在打开编译选项 strictNullChecks 时才有意义,否则编译器不会检查某个变量是否可能为空(即 undefinednull)。

断言函数

  1. 断言函数:该函数是一种特殊函数,用于保证函数参数符合某种类型。如果函数参数符合指定类型,则不进行任何操作;否则,就会抛出错误,中断程序执行。

    • 断言函数命名为 isType,要断言的参数 value 的类型为 unknown,函数体中包含断言逻辑。
    • 断言函数有两种写法,
      • 旧写法的返回值类型为 void
      • 新写法的返回值类型为 asserts value is type(相当于 void 类型,assertsis 都是关键字,value 时要断言的函数参数名,type 时函数参数的预期类型)
  2. 语法

    • 旧写法

      1
      2
      3
      4
      function isString(value: unknown): void {
      if (typeof value !== 'string')
      throw new Error('Not a string');
      }
    • 新写法(推荐,更清晰地表达函数意图,即该函数为断言函数)

      1
      2
      3
      4
      function isString(value: unknown): asserts value is string {
      if (typeof value !== 'string')
      throw new Error('Not a string');
      }
  3. 使用说明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // 1. 如果断言函数内部的检查与预期类型不一致,TypeScript 不会报错。
    function isString1(value: unknown): asserts value is string {
    if (typeof value !== 'number')
    throw new Error('Not a number');
    }

    // 2. 断言函数的 asserts 语句等同于 void 类型,所以如果返回 undefined 或 null 以外的值,TypeScript 就会报错。
    function isString2(value: unknown): asserts value is string {
    if (typeof value !== 'string')
    throw new Error('Not a string');
    return true; // ❌
    }

    // 3. 可以借助工具类型 NonNullable<T> 来断言函数参数非空。
    // 补充:NonNullable<T> 对应类型 T 去除空类型(undefined 或 null)后的剩余类型。
    function assertsIsDefined<T>(value: T): asserts value is NonNullable<T> {
    if (value === undefined || value === null)
    throw new Error(`${value} is not defined`);
    }

    let x = null;
    assertsIsDefined(x); // 此时代码执行时会抛出错误

    // 4. 与断言函数不同,类型保护函数也可用于检查函数参数是否符合某种类型,但是其返回值为布尔值。
    // 补充:类型保护函数命名为 isType,要保护的参数 value 的类型为 unknown,返回值类型为 value is type(相当于 boolean 类型),函数体中包含检查逻辑。
    function isString3(value: unknown): value is string {
    return typeof value === 'string';
    }

    // 5. 可以使用断言函数的简写形式(asserts value)来断言某个参数为真(即不等于 false,undefind,null)。
    // 补充:断言函数的简写形式通常也可用来检查某个操作是否成功。
    function assert(value: unknown): asserts value {
    if (!value)
    throw new Error(`${value} should be a truthy value.`)
    }

ES6 - 模块

注意:这里所说接口指的是模块向外暴露的变量、类、函数

简要概述

  1. ES6 模块化与 CommonJS 和 AMD 模块化的区别

    • ES6 模块化在编译时加载模块,其模块是通过 export 命令显式输出的代码,可以直接加载指定的接口。由于 ES6 模块在编译时加载,因此可以对代码进行静态分析,如引入宏和类型检验等。
    • CommonJS 和 AMD 模块化在运行时加载模块,其模块是一个对象,必须加载完整个对象后,再从该对象身上读取指定的接口。
  2. ES6 模块的特点

    • ES6 使用 export 命令导出模块的接口,import 命令导入其他模块提供的接口。

    • ES6 中一个文件就是一个模块,该文件中的所有接口,外部都无法获取(除非使用 export 命令导出接口)。

    • ES6 模块自动采用严格模式,即默认在模块头部添加 "use strict"

      补充:严格模式在 ES5 引入。

    • ES6 模块的顶层作用域this 指向 undefined,因此不应该在顶层作用域中使用 this

模块语法

具名导出与导入

  1. 具名导出

    • 语法

      • 写法一:声明时导出

        1
        2
        3
        export var day = 1;
        export const month = 1;
        export let year = 2025;
      • 写法二:先声明,再导出(推荐,因为可以写在脚本尾部,更清晰)

        1
        2
        3
        4
        5
        var day = 1;
        const month = 1;
        let year = 2025;

        export { day, month, year }
      • 写法三:先声明,再导出,同时设置别名(允许给一个变量指定多个别名)

        1
        2
        3
        4
        5
        6
        export {
        day as iday,
        month as imonth,
        year as iyear,
        year as jyear
        }
    • 使用说明

      • 与 CommonJS 不同,ES6 通过 export 命令导出的接口与其对应的值是动态绑定的,即可以通过该接口访问到模块内部实时的值
      • export 命令可以出现在模块中的任何位置,但一定要处于顶层作用域。
  2. 具名导入

    • 语法

      • 写法一:直接导入接口

        1
        import { day, month, year } from './xxx.js'
      • 写法二:直接导入接口,同时设置别名

        1
        import { day as iday, month as imonth, year as iyear } from './xxx.js'
    • 使用说明

      • import 命令导入的接口是只读的。虽然可以修改接口的属性,但是千万不要这么做,因为其他模块导入该接口时,会访问到被改写后的属性!

      • from 命令用于指定模块的位置,其可以是相对路径、绝对路径或模块名。如果使用模块名,则必须包含配置文件,以告诉 JavaScript 引擎该模块的位置。

      • import 命令会被提升到整个模块顶部首先执行。

      • import 命令会执行所加载的模块,因此允许直接使用 import '模块位置' 的方式执行模块。

      • 多次加载一个模块只会执行一次,可以理解为 import 语句是 singleton 模式的。

        1
        2
        3
        4
        5
        import 'lodash';
        import 'lodash';

        // 等价于
        import 'lodash';
        1
        2
        3
        4
        5
        import { foo } from 'my_module';
        import { bar } from 'my_module';

        // 等价于
        import { foo, bar } from 'my_module';
      • 虽然 Babel 允许在同一模块中混用 CommonJS 的 require 和 ES6 的 import,但不推荐这样做。因为 ES6 模块(静态加载)总是早于 CommonJS 模块(动态加载)执行,可能导致意外结果。

默认导出与导入

  1. 解释:不同于具名导出,ES6允许使用 export default 为模块设置默认导出接口。当其他模块加载此模块时,可以为此默认导出接口指定任何名称

  2. 语法

    • 默认导出

      1
      2
      3
      4
      5
      6
      export default function() { /* ... */ }

      // OR

      function foo() { /* ... */ }
      export default foo;
    • 默认导入

      1
      import customName from './xxx.js'
  3. 使用说明

    • 每个模块只能有一个默认输出,这意味着在一个模块中,export default 命令只能使用一次。

    • export default xxx 本质上是将模块的特定接口赋值给一个名为 default 的变量并具名导出。当其他模块导入这个 default 变量时,ES6 允许使用任何名称来引用该变量。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      // example1
      function add(x, y) {
      return x * y;
      }
      export { add as default }; // 等价于 export default add;

      // example2
      import { default as foo } from "./xxx.js" // 等价于 import foo from "./xxx.js"

      // example3
      var a = 1;
      export default a; // 等价于 var default = a; export { default };

      // example4
      export default 124; // 等价于 var default = 124; export {default};

      // example5
      export default var a = 1; // ❌,因为默认导出的本质是赋值,显然 var default = var a 的赋值方式是错误的,所以这里报错
    • 默认导入和具名导入可以同时使用,表示同时导入默认接口和具名接口。

      1
      import customName, { foo } from './xxx.js'

整体导入

  1. 解释:整体导入是指将模块的所有公开接口加载到一个以星号 * 表示的对象上。在使用时,需要为此对象设置别名。这样,被导入模块的所有公开接口都将被加载到这个别名对象上。

  2. 语法

    1
    import * as customName from './xxx.js'
  3. 使用说明

    • 通过模块整体导入创建的对象在运行时不可更改,因为这会破坏 ES6 的静态加载特性。

重导出

注意:这里的默认接口指的是默认导出的接口,具名接口指的是具名导出的接口

  1. 解释:在 ES6 中,import 命令和 export 命令可以复合使用,这表示先导入然后再导出同一个模块的接口,称之为重导出。实际上,这些接口并没有真正被导入到当前模块,而只是在当前模块进行了接口的转发

  2. 语法

    1
    2
    3
    4
    export * from './xxx.js'
    export { xxx } from './xxx.js'
    export { xxx as customName } from './xxx.js'
    export * as customName from './xxx.js' // ES2020 新增写法

    注意:export customName from './xxx.js' 的语法是非法的

  3. 应用示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // example1 将具名接口改名后转发
    export { foo as myFoo } from './xxx.js'

    // example2 将所有接口(具名接口)整体转发
    export * from './xxx.js'

    // example3 将默认接口转发
    export { default } from './xxx.js'

    // example4 将具名接口改为默认接口后转发
    export { es6 as default } from './xxx.js'

    // example5 将默认接口改为具名接口后转发
    export { default as es6 } from './xxx.js'

    // example5 将所有接口(默认接口 + 具名接口)封装为命名空间后转发
    export * as customName from './xxx.js'

跨模块常量

  1. 解释:跨模块常量是指被导出并在多个模块间共享的常量。

  2. 实践:为了管理跨模块常量,我们通常在项目中创建一个 constants 目录,并将不同用途的常量保存在该目录下的不同文件中。在 constants 目录中,我们可以创建一个 index.js 文件,用于将其他文件导出的常量合并并重新导出(重导出)。这样,当需要使用跨模块常量时,只需要加载 constants/index.js 文件即可。

    1
    2
    3
    4
    5
    6
    // constants/db.js
    export const db = {
    url: 'http://my.couchdbserver.local:5984',
    admin_username: 'admin',
    admin_password: 'admin password'
    };
    1
    2
    // constants/user.js
    export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];
    1
    2
    3
    // constants/index.js
    export { db } from './db';
    export { users } from './users';
    1
    2
    // script.js
    import {db, users} from './constants/index';

运行时加载

  1. 解释:ES6 模块化通过 import 命令实现静态加载模块,也就是在编译时加载模块。 import 命令会在模块中最先执行,如果它位于代码块(如条件语句)中,将会引发语法错误。然而,为了实现动态加载模块,也就是在运行时加载模块,ES2020 引入了 import() 函数。这个函数接受一个参数 specifier,表示模块的位置,然后在运行时加载指定的模块,并返回一个 Promise 对象。import() 函数的功能类似于 Node.js 的 require 方法。

  2. 语法:import(specifier)

    specifier 指定了模块位置,与 import 命令相同,可以是相对路径、绝对路径或模块名。

  3. 使用说明

    • import() 函数的使用时机有:按需加载模块条件加载模块模块的路径需要动态生成
    • import() 函数可以用在任何地方,包括非模块脚本(如 type 属性不为 modulescript 标签中的脚本)。
    • import() 函数成功动态加载模块后,该模块会被作为一个对象传递给 then 方法作为参数。模块中导出的**所有接口(包括默认接口)**都会作为这个对象的属性存在。
    • 如果需要同时动态加载多个模块,你可以使用 Promise.all() 方法。在这种情况下,then 方法的参数将是一个由这些模块对象构成的数组。

模块元信息

  1. 解释:ES2020 引入了 import.meta,允许在当前模块中访问模块的元信息import.meta 是一个只能在模块内部使用的对象,它的属性取决于运行环境,没有统一的规定。

  2. 常用属性

    • import.meta.url:当前模块的完整 URL。该属性可以用于构建与当前模块相关的资源路径。

    • import.meta.scriptElement:加载当前模块的 script 元素,相当于 document.currentScript(浏览器特有)。

    • import.meta.filename:当前模块的绝对路径,相当于 CommonJS 的 __filename(Deno 特有)。

    • import.meta.dirname:当前模块所在目录的绝对路径,相当于 CommonJS 的 __dirname(Deno 特有)。

      补充:Deno 是一个 JavaScript 和 TypeScript 运行时,默认使用 ES 模块化,且内置 TypeScript 支持。

ES6 - 模块加载

浏览器加载

  1. 加载非模块脚本

    1
    2
    3
    4
    5
    6
    7
    <!-- 内嵌脚本 -->
    <script type="application/javascript">
    // 脚本内容
    </script>
    <!-- 外部脚本 -->
    <script type="application/javascript" src="path/to/myModule.js">
    </script>
    • 浏览器脚本的默认语言是 JavaScript,因此 type="application/javascript" 可以省略

    • 默认情况下,浏览器同步加载 JavaScript 脚本,即当渲染引擎遇到 <script> 标签时就会停下来,等到执行完脚本,再继续向下渲染。对于外部脚本,还需要包含脚本下载的时间。

    • 如果 <script> 标签使用 deferasync 属性,浏览器会异步加载 JavaScript 脚本。

      比较 defer async
      执行时机 渲染完再执行
      (脚本会在 DOM 结构完全生成后再执行)
      下载完就执行
      (脚本会在下载完后立即执行,此时会使渲染引擎中断渲染)
      顺序执行
      (多个 defer 脚本会根据其在页面出现的顺序执行)

      (多个 async 脚本不能保证执行顺序)
  2. 加载 ES6 模块脚本(使用 type="module" 属性)

    1
    2
    3
    4
    5
    6
    7
    <!-- 内嵌脚本 -->
    <script type="module">
    // 脚本内容
    </script>
    <!-- 外部脚本 -->
    <script type="module" src="path/to/myModule.js">
    </script>
    • 浏览器异步加载 ES6 模块,相当于自带了 defer 属性。此时,模块脚本会等待整个页面渲染完再执行,且多个模块脚本会按照其在页面出现的顺序执行。但是如果此时使用了 async 属性,模块脚本只要下载完成就会立即执行,且多个模块脚本之间的执行顺序也无法保证。
    • 模块脚本的特点
      • 模块作用域:代码运行的作用域是模块作用域,而不是全局作用域。因此模块内部的顶层变量,外部不可见。
      • 严格模式:自动使用严格模式(相当于声明了 “use strict”),此时模块顶层 this 的值是 undefined
      • 导入导出:可以使用 import 命令加载其他模块(不能省略 .js 后缀),使用 export 命令向外暴露接口。
      • 执行一次:同一个模块加载多次时,只执行一次。

Node.js 加载

加载区别

Node.js 支持 CommonJS 模块(CJS)和 ES6 模块(MJS),二者不兼容,区别有

  • CommonJS 模块使用 requiremodule.export;ES6 模块使用 importexport

  • CommonJS 模块采用 .cjs 后缀;ES6 模块采用 .mjs 后缀。

    package.json 文件中,如果指定 "type": "module",那么 .js 文件以 ES6 模块加载;如果指定 "type": "commonjs" 或不指定,则 .js 文件以 CommonJS 模块加载。

    ES6 模块与 CommonJS 模块不要混用!


Node.js 加载 ES6 模块的注意事项

  • 不能省略脚本的后缀名,包括 import 命令和 package.json 中 main 字段指定的脚本路径。

    1
    2
    import { something } from './index.js'; // ✅
    import { something } from './index'; // ❌
  • .mjs 文件支持 URL 路径,即路径中可以包含查询参数。同一个脚本只要参数不同,就会被加载多次,并且保存成不同的缓存。因此,只要文件名中含有 :%#? 等特殊字符,最好对这些字符进行转义

    1
    import './foo.mjs?query=1';
  • Node.js 的 import 命令仅支持加载本地模块(使用 file: 协议)和 data: 协议。远程模块加载(例如通过 http:https:)是不支持的。

  • 在使用 import 时,只支持相对路径,也就是说路径必须以 ./../ 开头。绝对路径(以 /// 开头)是不支持的。


Node.js 中不能在 ES6 模块中使用,而可以在 CommonJS 模块中使用内部顶层变量有,

  • this(在 ES6 模块中,顶层 this 指向 undefined;在 CommonJS 中,顶层 this 指向当前模块)
  • arguments
  • require
  • module
  • exports
  • __filename
  • __dirname

入口配置

package.json 有两个字段可以指定模块的入口文件mainexports,其中 exports 字段的用法更加复杂,优先级main 字段。

  1. main 指定模块加载的入口

    1
    2
    3
    4
    5
    // ./node_modules/es-module-package/package.json
    {
    "type": "module",
    "main": "./src/index.js"
    }
    1
    2
    3
    4
    // ./my-app.mjs

    import { something } from 'es-module-package';
    // 加载 ./node_modules/es-module-package/src/index.js
  2. exports 指定脚本或子目录别名

    1
    2
    3
    4
    5
    6
    7
    // ./node_modules/es-module-package/package.json
    {
    "exports": {
    "./submodule": "./src/submodule.js", // 脚本别名
    "./features/": "./src/features/" // 子目录别名
    }
    }
    1
    2
    3
    4
    5
    6
    7
    // ./my-app.mjs

    import submodule from 'es-module-package/submodule';
    // 加载 ./node_modules/es-module-package/src/submodule.js

    import feature from 'es-module-package/features/x.js';
    // 加载 ./node_modules/es-module-package/src/features/x.js

    注意:如果没有通过 exports 关键字指定别名,此时就不能使用 “模块名+脚本名” 来加载模块中的指定脚本。

    1
    2
    3
    4
    5
    6
    7
    // ./my-app.mjs

    // 报错 ❌
    import submodule from 'es-module-package/private-module.js';

    // 不报错 ✅
    import submodule from './node_modules/es-module-package/private-module.js';
  3. exports 指定模块加载的主入口,优先级main 字段。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "exports": {
    ".": "./main.js" // 完整写法,别名 "." 表示模块的主入口
    }
    }

    // 等同于
    {
    "exports": "./main.js" // 简写形式,"exports" 直接表示模块的主入口
    }

    注意:exports 字段只有支持 ES6 的 Node.js 才认识,因此可以搭配 main 字段,来兼容旧版本的 Node.js。

    1
    2
    3
    4
    5
    6
    {
    "main": "./main-legacy.cjs", // 老版本 Node.js 的入口文件
    "exports": {
    ".": "./main-modern.cjs" // 新版本 Node.js 的入口文件
    }
    }
  4. exports 指定 CommonJS 和 ES6 的模块加载的主入口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    "type": "module",
    "exports": {
    ".": {
    "require": "./main.cjs", // 使用 require() 命令的入口文件 - CommonJS 入口
    "default": "./main.js" // 使用 import 命令的入口文件 - ES6 入口
    }
    }
    }

    // 等同于
    {
    "exports": {
    "require": "./main.cjs", // 简写形式
    "default": "./main.js" // 简写形式
    }
    }

    注意:如果通过 exports 关键字还设置了其他别名,此时就不能使用简写形式来进行条件加载。

    1
    2
    3
    4
    5
    6
    7
    8
    {
    // 报错 ❌
    "exports": {
    "./feature": "./lib/feature.js",
    "require": "./main.cjs",
    "default": "./main.js"
    }
    }

兼容加载

  1. CommonJS 模块中加载 ES6 模块:使用 import() 方法。

    1
    2
    3
    (async () => {
    await import('./my-app.mjs');
    })();
  2. ES6 模块中加载 CommonJS 模块:使用 import 命令整体加载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 正确 ✅
    import packageMain from 'commonjs-package';

    // 报错 ❌
    import { method } from 'commonjs-package';

    // 正确 ✅(加载单一的输出项)
    import packageMain from 'commonjs-package';
    const { method } = packageMain;

    另一种不建议的在 ES6 模块中加载 CommonJS 模块的方法是,使用 Node.js 内置的 module.createRequire() 方法。这种写法将 ES6 和 CommonJS 用法混合,因此不建议使用。

    1
    2
    3
    import { createRequire } from 'module';
    const require = createRequire(import.meta.url);
    const cjs = require('./xxx.cjs');
  3. Node.js 的内置模块使用 import 命令加载时,既可以整体加载,也可以加载指定的输出项

    1
    2
    3
    4
    5
    // 整体加载
    import EventEmitter from 'events';

    // 加载指定的输出项
    import { readFile } from 'fs';

兼容支持

  1. ES6 模块允许被 CommonJS 模块加载:在 ES6 模块中添加一个整体输出接口,如 export default obj

  2. CommonJS 模块允许被 ES6 模块加载:给 CommonJS 模块添加一个包装层(是一个符合 ES6 模块化规范的文件),该文件中先整体输入 CommonJS 模块,再根据需要输出具名接口

    1
    2
    import cjsModule from '../index.js';
    export const foo = cjsModule.foo;

    为了让包装层符合 ES6 模块化规范,可以把这个文件的后缀名改为 .mjs,或者将它放在一个子目录,再在这个子目录里面放一个单独的 package.json 文件,指明 { type: "module" }

  3. 如果希望一个模块同时被 require()import 加载,那么可以使用 package.json 中的 exports 字段设置条件加载,即指明不同格式的模块各自加载的入口

ES6 模块化 Vs. CommonJS 模块化

区别 ES6 CommonJS
输出类型 值的引用 值的拷贝
加载时机 编译时加载 运行时加载
加载方式 import 异步加载 require() 同步加载

进一步解释

  1. CommonJS 加载的模块是通过 module.exports 暴露出的对象,该对象在脚本运行完后生成。ES6 加载的模块是一种静态定义,在代码静态解析阶段生成。

  2. CommonJS 模块输出的是值的拷贝,ES6 模块输出的是值的引用,通过以下代码可以清楚感知到其区别!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // lib.js
    var counter = 3;
    function incCounter() {
    counter++;
    }
    module.exports = {
    counter: counter,
    incCounter: incCounter,
    };
    1
    2
    3
    4
    5
    6
    // main.js
    var mod = require('./lib'); // or import mod from './lib'

    console.log(mod.counter); // 3
    mod.incCounter();
    console.log(mod.counter); // 3 for CommonJS; 4 for ES6
    • CommonJS 输出分析:mod.incCounter() 方法的调用并未影响 mod.counter 的值,这是因为加载模块时,会对模块所有暴露的接口进行缓存。如果想通过 lib.js 模块的 incCounter() 方法来修改暴露的 counter,需要将 counter 属性设置为一个 getter

      1
      2
      3
      4
      5
      6
      7
      // lib.js
      module.exports = {
      get counter() {
      return counter;
      }
      incCounter: incCounter,
      };
    • ES6 输出分析:mod.incCounter() 方法的调用影响了 mod.counter 的值,这是因为 JS 在进行静态分析时,遇到 import 命令后,会生成一个只读引用。当脚本执行时,会通过这个只读引用去加载的模块取值。ES6 模块不会缓存运行结果,而是动态地从被加载的模块中取值,因此引入的模块接口都是只读的。

  3. 其他关于 CommonJS-值的拷贝 与 ES6-值的引用 区别的示例代码如下,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // mod.js
    function C() {
    this.sum = 0;
    this.add = function () {
    this.sum += 1;
    };
    this.show = function () {
    console.log(this.sum);
    };
    }

    export let c = new C();
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // x.js
    import {c} from './mod';
    c.add();

    // y.js
    import {c} from './mod';
    c.show();

    // main.js
    import './x';
    import './y'; // 0 for CommonJS; 1 for ES6

循环加载

循环加载(circular dependency)指的是,a 脚本的执行依赖 b 脚本,而 b 脚本的执行又依赖 a 脚本。

  1. CommonJS 模块化中的循环加载

    • 模块加载原理:在 CommonJS 中,当你使用 require 命令第一次加载特定模块时,该模块会被执行,并在内存中生成一个对象。之后,当你需要使用这个模块时,就会从这个内存对象的 exports 属性中取值。即使你再次执行 require 命令,该模块也不会再次执行,而是直接从缓存中取值。因此,无论你加载一个 CommonJS 模块多少次,它都只会在第一次加载时运行一次,之后的加载都会返回第一次运行的结果,除非你手动清除系统缓存。

      1
      2
      3
      4
      5
      6
      {
      id: '...', // 模块名称
      exports: { ... }, // 模块输出的各个接口
      loaded: true, // 表示该模块的脚本是否执行完毕
      ...
      }
    • 循环加载策略:一旦出现某个模块被 “循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // a.js
      exports.done = false; // <1>
      var b = require('./b.js'); // <2>
      /*
      模块 a 暂时挂起,等待模块 b 加载完成。
      此时 a 的 exports 属性已经初始化,但可能还未达到最终状态。
      */
      console.log('在 a.js 之中,b.done = %j', b.done); // <8>
      exports.done = true; // <9>
      console.log('a.js 执行完毕'); // <10>
      /* 模块 a 的执行完成 */
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // b.js
      exports.done = false; // <3>
      var a = require('./a.js'); // <4>
      /*
      此时模块 a 被循环加载,只能访问 a 的当前导出状态,即 exports: { done: false }。
      在循环加载时,不会暂停当前模块的执行,而是使用模块 a 已经导出的值。
      */
      console.log('在 b.js 之中,a.done = %j', a.done); // <5>
      exports.done = true; // <6>
      console.log('b.js 执行完毕'); // <7>
      /*
      模块 b 执行完毕后,控制权返回给模块 a,模块 a 继续执行。
      */
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // main.js
      var a = require('./a.js');
      var b = require('./b.js');
      console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
      /* Output:
      在 b.js 之中,a.done = false
      b.js 执行完毕
      在 a.js 之中,b.done = true
      a.js 执行完毕
      在 main.js 之中, a.done=true, b.done=true
      */
    • 安全的模块加载方式

      1
      2
      var a = require('a'); // 安全的写法 ✅
      var foo = require('a').foo; // 危险的写法(如果发生循环加载,值后续可能会被改写) ❌
  2. ES6 模块化中的循环加载

    • 循环加载策略:一旦某个模块被 “循环加载”,就会认为使用的该模块的相关接口已经存在,继续向下执行。

      1
      2
      3
      4
      5
      // a.mjs
      import {bar} from './b'; // <1>
      console.log('a.mjs'); // <6>
      console.log(bar()); // <7>
      export let foo = 'foo'; // <8>
      1
      2
      3
      4
      5
      // b.mjs
      import {foo} from './a'; // <2>
      console.log('b.mjs'); // <3>
      console.log(foo()); // <4> 未定义报错
      export let bar = 'bar'; // <5>

TS - 模块

简要概述

任何包含 importexport 语句的文件,就是一个模块(module)。

  • 如果文件不包含 export 语句,就是一个全局的脚本文件

  • 模块本身就是一个作用域,不属于全局作用域。

  • 暴露给外部的接口,必须用 export 命令声明;如果其他文件要使用模块的接口,必须用 import 命令来输入。

  • 如果一个文件不包含 export 语句,但是希望把它当作一个模块(即内部变量对外不可见),可以在脚本头部添加一行语句如下,此时当前文件被当作模块处理。

    1
    export {};
  • TypeScript 允许加载模块时,省略模块文件的后缀名

模块语法

注意-1:这里的类型包括 typeinterface 关键字声明的类型。

注意-2:TypeScript 中通过 export 导出的内容分为正常接口类型

TypeScript 支持所有 ES6 模块语法,此外还支持类型的导出和导入。

注意:以下示例中,假设 A 是类型,a 是正常接口(变量、类、函数)。

  1. 类型导出

    • 语法一(不推荐,难以区分类型和正常接口)

      1
      2
      3
      4
      5
      6
      7
      export interface A { foo: string; }
      export let a = 1;

      // 等同于(简写形式)
      interface A { foo: string; }
      let a = 1;
      export { A, a };
    • 语法二

      1
      export { type A, a };
    • 语法三

      1
      2
      export type { A };
      export { a };
  2. 类型导入

    • 语法一(不推荐,难以区分类型和正常接口)

      1
      import {A, a} from './my_module'
    • 语法二

      1
      import { type A, a } from './my_module'
    • 语法三

      1
      2
      import type { A } from './my_module';
      import { a } from './my_module';
  3. 使用说明

    • TypeScript 还支持默认导出导入和整体导入

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // 默认导出
      export default interface User { name: string }; // 默认导出 - interface 定义的类型

      type Stu = { grade: number }; // 默认导出 - type 定义的类型
      export default Stu;

      // 默认导入 - 引入模块的默认导出类型
      import type DefaultType from './my_module'

      // 整体导入 - 将模块的所有导出类型作为一个命名空间引入
      import type * as TypeNS from './my_module'
    • 对于一个类,如果使用 export { className } 导出,那么导出的是一个正常接口;如果使用 export type { className } 导出,那么导出的是一个类型

CommonJS 模块处理

  1. 模块导入

    • import= 语句

      1
      import fs = require('fs');
    • import * as [模块名] from "模块地址"

      1
      import * as fs from 'fs';
  2. 模块导出:export= 语句

    1
    2
    3
    let obj = { foo: 123 };

    export = obj;

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

类型编译

TypeScript 提供编译选项 importsNotUsedAsValues 来指定导入类型import 命令的编译方式。该编译选项的可取值为,

  • remove:默认值。删除导入类型的 import 语句。
  • preserve:保留类型的 import 语句,将其变为 “import 模块位置” 的形式,从而保留模块中的副作用。
  • error:保留类型的 import 语句,与 preserve 相同,但是导入类型必须使用 import type 的形式,否则报错。

假设 .ts 脚本中有以下代码,

1
import { TypeA } from './a';
  • "importsNotUsedAsValues": "remove" 时,编译结果为空。

  • "importsNotUsedAsValues": "preserve" 时,编译结果为

    1
    import './a';
  • "importsNotUsedAsValues": "error" 时,编译报错,除非原始的 .ts 脚本中的代码如下,此时编译结果与 "preserve" 的编译结果相同。

    1
    import type { TypeA } from './a';

模块定位

模块定位(module resolution)指的是,一种用来确定 import 语句和 export 语句里面的模块文件位置的算法。

相对、非相对模块

  1. 相对模块(relative import):指的是路径以 /./../ 开头的模块。相对模块根据当前脚本所在位置进行计算,通常用于保存在当前项目目录结构中的模块脚本。

    1
    2
    3
    import Entry from "./components/Entry"; // 相对路径
    import { DefaultHeaders } from "../constants/http"; // 相对路径
    import "/mod"; // 绝对路径
  2. 非相对模块(non-relative import):指的是不带有路径信息的模块。非相对模块根据 baseUrl 属性或模块映射来确定,通常用于加载外部模块。

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

Classic、Node 方法

TypeScript 使用编译参数 moduleResolution确定模块定位算法,常用的算法有两种:ClassicNode。其中 Node 方法就是模拟 Node.js 的加载方法,即 require() 的实现方法。

根据编译参数 module 值的不同,编译参数 moduleResolution 有不同的默认值,"module": "commonjs" 时,默认值为 "node",否则为 "classic"

模块定位方法 Classic 方法 Node 方法
相对模块 以当前脚本的路径作为基准路径,计算相对模块的位置。 以当前脚本的路径作为基准路径
1. 首先检查当前目录中是否存在与模块名称对应的 .ts.tsx.d.ts 文件。如果找到,直接加载该文件。
2. 如果没有找到对应的文件,接下来检查当前目录中是否存在与模块名称对应的目录。 如果该目录存在,进一步检查目录下的 package.json 文件。 如果 package.json 文件存在,并且 types 字段指定了模块的入口文件,则加载该入口文件。
3. 如果 package.json 文件不存在或 types 字段未指定入口文件,查找该目录下的 index.tsindex.tsx、或 index.d.ts 文件。 如果找到其中一个文件,则加载该文件。
4. 如果以上步骤均未成功找到模块,则抛出错误,提示无法找到指定模块。
非相对模块 以当前脚本的路径作为起点,一层层查找上级目录。 1. 首先检查当前目录的 node_modules 子目录中是否存在文件 .ts.tsx.d.ts。如果找到,直接加载该文件。
2. 如果没有找到对应的文件,检查 node_modules 子目录中是否存在 package.json 文件。 如果 package.json 文件存在,并且 types 字段指定了入口文件,则加载该入口文件。
3. 如果 package.json 文件没有指定入口文件,检查 node_modules 中是否存在 @types 子目录。 在 @types 目录中查找 .d.ts 文件,如果找到,则加载该文件。
4. 如果没有找到 @types 目录或文件,检查 node_modules 中是否存在与模块名称相同的子目录。 在子目录中查找 index.tsindex.tsxindex.d.ts 文件。如果找到其中一个文件,则加载该文件。
5. 如果在当前目录中未找到模块,进入上一层目录,并重复上述步骤。 继续向上查找,直到找到模块或到达文件系统的根目录为止。

路径映射配置

TypeScript 提供了以下 tsconfig.json 中的配置,来指定脚本模块的路径

  1. compilerOptions.baseUrl:指定脚本模块的基准目录

    1
    2
    3
    4
    5
    {
    "compilerOptions": {
    "baseUrl": "." // "." 表示基准目录是 tsconfig.json 所在的目录。
    }
    }
  2. compilerOptions.paths:指定非相对模块与实际脚本的映射,常与 baseUrl 配合使用。

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "compilerOptions": {
    "baseUrl": ".",
    "paths": {
    "jquery": ["node_modules/jquery/dist/jquery"] // 加载 jquery 时,实际加载的脚本为 node_modules/jquery/dist/jquery(结合 baseUrl 进行计算)
    }
    }
    }

    注意:每个非相对模块名的值是一个数组,可以指定多个路径。如果第一个脚本路径不存在,那么就加载第二个路径,以此类推。

  3. compilerOptions.rootDirs:指定模块定位时需要查找的其他目录。该配置指定一个目录列表,此时这些目录在编译时被视为一个虚拟的根目录,也就是说 TypeScript 编译器会将这些目录中的文件当作在同一个目录中一样处理

    1
    2
    3
    4
    5
    6
    {
    "compilerOptions": {
    "rootDirs": ["src/zh", "src/de", "src/#{locale}"]
    }
    }

模块编译参数

  1. --traceResolution 参数,表示编译时在命令行显示模块定位的每一步

    1
    $ tsc --traceResolution
  2. --noResolve 参数,表示模块定位时,只考虑在命令行传入的模块

    1
    2
    import * as A from "moduleA";
    import * as B from "moduleB";
    1
    $ tsc app.ts moduleA.ts --noResolve # 报错,因为 moduleB 模块无法被定位

TS - 命名空间

基本使用

  1. 命名空间:namespace 是 TypeScript 在 ES6 之前用来组织相关代码(模块)的方式。TypeScript 允许使用 namespace 关键字创建一个容器,内部的所有变量和函数,都必须在这个容器里边使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    namespace AssertUtils {
    function isString(value: unknown): value is string {
    return typeof value === 'string';
    }

    console.log(isString(123)); // ✅
    }

    // console.log(AssertUtils.isNumber('123')); // ❌
  2. 语法 namespace customName { ... }

  3. 使用说明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    // 1. 给命名空间的内部成员加上 export 前缀,此时就可以在命名空间外部访问命名空间的内部成员。
    namespace Constants {
    export const PI = Math.PI;
    export const E = Math.E;
    }

    console.log(Constants.PI, Constants.E);

    // 2. 命名空间的编译结果为一个 JavaScript 对象,此时凡命名空间中包含 export 前缀的内部成员,都成为了该对象的属性。
    // 比如,Constants 的编译结果为 { "PI": 3.141592653589793, "E": 2.718281828459045 }

    // 3. 可以在命名空间中使用 import 命令导入其他命名空间的成员,同时为该成员设置别名。也可以在命名空间外部使用 import 命令,同时设置别名。
    namespace MathConstants {
    import MathPI = Constants.PI;
    import MathE = Constants.E;

    console.log("in MathConstants", MathPI, MathE);
    }

    import PI = Constants.PI;
    console.log('import from Constants', PI);

    // 4. 命名空间可以嵌套,但是如果要在外部使用被嵌套的命名空间,则要给其添加 export 前缀。
    namespace Jack {
    export namespace DetailInfo {
    export let age = 18;
    }
    }

    console.log('jack\'s age is', Jack.DetailInfo.age)

    // 5. 命名空间中不仅可以包含实义代码,还可以包括类型代码。
    namespace N {
    export interface MyInterface { };
    export type MyType = {};
    export class MyClass { };
    }

    // 6. 命名空间与模块的作用一致,都用于将相关代码组织在一起,并对外输出接口。一个文件只能有一个模块(更建议使用),但可以有多个命名空间。

    // 7. 可以使用三斜杠指令来引入存放在单个文件中的命名空间代码。
    /// <reference path = "SomeFileName.ts">

    // 8. 命名空间可以使用 export 命令导出,在其他文件中使用 import 命令导入。
    export namespace Shapes {
    export class Triangle {
    // ...
    }
    }
    /*
    其他脚本中使用 import 语法如下导入命名空间 Shapes 的 Triangle 类
    - 方法一
    import { Shapes } from "./xxx.ts"
    const t = new Shapes.Triangle();
    - 方法二
    import * as shapes from './xxx.ts
    const t = new shapes.Shapes.Triangle();
    */

命名空间合并

  1. 命名空间合并:与 interface 类似,多个同名的 namespace 会自动合并,这有利于对代码的扩展。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    namespace Animals {
    export class Cat { }
    }

    namespace Animals {
    export class Dog { }
    }

    // 等同于
    /*
    namespace Animals {
    export class Cat{}
    export class Dog{}
    }
    */
  2. 使用说明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 1. 命名空间合并时,没有使用 export 前缀的成员不会被合并,其只能在各自的命名空间中使用。

    // 2. 命名空间可以和同名函数、类、Enum 合并,但是函数、类、Enum 必须在命名空间之前声明,此时同名的命名空间相当于给这个函数、类、Enum 对象添加额外的属性。
    function f() {
    console.log((f as any).version);
    }

    namespace f {
    export const version = '1.0'
    }

    f(); // '1.0'

    // 3. 命名空间合并时,命名空间中使用 export 前缀的成员不能与 Enum 成员同名。
    enum Direction {
    Left,
    Right
    }

    namespace Direction {
    export const Left = 1; // ❌
    }

TS - 装饰器(标准语法)

简要介绍

  1. 装饰器(Decorator):一种特殊的函数,可以附加到类、方法、访问器、属性上,用于修改或扩展其行为

    • 装饰器是函数

    • 装饰器通过 "@ + 函数""@ + 工厂函数(参数)" 的方式使用(工厂函数就是会返回一个函数的函数)

    • 装饰器接受所修饰对象的一些相关值作为参数

    • 装饰器要么不返回值,要么返回一个新对象来取代所修饰的对象

    • 装饰器只能应用于类及其内部成员,不能用于独立的函数

  2. 装饰器版本:TypeScript 支持两种装饰器语法:

    • 传统语法:TypeScript 最初支持的语法。适用于 TypeScript 4.x 或更早版本,需要配置如下,

      1
      2
      3
      4
      5
      6
      {
      "compilerOptions": {
      "experimentalDecorators": true, // must
      "emitDecoratorMetadata": true // could 该编译选项用于控制是否产生一些装饰器的元数据,供某些模块(如 reflect-metadata)使用
      }
      }
    • 标准语法:符合 ECMAScript 标准的语法。适用于 TypeScript 5.0 或更高版本,需要配置如下,

      1
      2
      3
      4
      5
      6
      {
      "compilerOptions": {
      "experimentalDecorators": false, // must
      "useDefineForClassFields": true // could 该编译选项用于控制是否使用 Object.defineProperty 来初始化类字段
      }
      }
  3. 装饰器的类型定义(函数)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    type Decorator = (
    target: DecoratedValue, // 所装饰的对象
    context: { // 上下文对象,包含有关所装饰对象的信息。可以使用 TypeScript 提供的原生接口 DecoratorContext 来描述
    kind: string; // 所装饰对象的类型,可取值为 'class', 'method', 'getter', 'setter', 'field', 'accessor',即对应六种类型的装饰器
    name: string | symbol; // 所装饰对象的名字,如属性名、类名等
    addInitializer?(initializer: () => void): void; // 添加类的初始化逻辑的函数
    static?: boolean; // 所装饰对象是否为类的私有成员
    private?: boolean; // 所装饰对象是否为类的静态成员
    access: { // 某个值的 getter 和 setter 方法
    get?(): unknown;
    set?(value: unknown): void;
    };
    }
    ) => void | ReplacementValue;

装饰器语法

类装饰器

  1. 类装饰器的类型定义

    1
    2
    3
    4
    5
    6
    7
    8
    type ClassDecorator<T extends Function> = (
    target: T, // 被装饰的类的构造函数
    context: {
    kind: 'class'; // 标识装饰器应用于类
    name: string | undefined; // 类名,如果是匿名类则为 undefined
    addInitializer(initializer: () => void): void; // 添加类初始化逻辑的方法,通常在类的构造函数执行完毕后立即执行
    }
    ) => T | void; // 可以返回一个新的类构造函数或不返回任何值

    注意:类装饰器其实就是构造方法装饰器

  2. 使用示例

    • 定义一个类装饰器,用于在所装饰的类的原型上添加一个 greet 方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      // 声明构造函数类型
      interface UserConstructor { new(...args: any[]): {} }

      // 扩展类的接口(避免 TypeScript 语法错误)
      interface User {
      greet(): void
      }

      // 定义类装饰器
      function Greeter(target: UserConstructor, context: ClassDecoratorContext) {
      if (context.kind === 'class') {
      target.prototype.greet = function () {
      console.log("Hello")
      }
      }
      }

      // 使用类装饰器
      @Greeter
      class User { }

      const u = new User();
      u.greet(); // 'Hello'
    • 定义一个类装饰器,返回一个函数,用于替代所装饰的类的构造方法,并在其中实现了实例的计数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      // 声明构造函数类型
      interface MyClassConstructor { new(...args: any[]): MyClass }

      // 扩展类的接口
      interface MyClass {
      count: number
      }

      // 定义类装饰器
      function countInstances(target: MyClassConstructor, context: ClassDecoratorContext) {
      let instanceCount = 0; // 实例统计变量,每创建一个实例对象,计数 + 1

      const wrapper = function (...args: any[]) { // 要返回的构造函数
      instanceCount++;
      const instance = new target(...args); // 构造函数创建的实例对象
      instance.count = instanceCount; // 实例属性 count,表示当前实例的编号
      return instance;
      } as unknown as typeof MyClass;
      wrapper.prototype = target.prototype; // 为了确保 wrapper 这个构造函数通过 instance of MyClass 运算符的检查

      return wrapper;
      }

      // 使用类装饰器
      @countInstances
      class MyClass { }

      const instance1 = new MyClass();
      const instance2 = new MyClass();
      console.log(instance1.count, instance2.count); // 1, 2
      console.log(instance1 instanceof MyClass); // true
    • 定义一个类装饰器,返回一个新类,用于替代所装饰的类,并在其中实现了实例的计数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      // 声明构造函数类型
      interface NewClassConstructor { new(...args: any[]): NewClass }

      // 扩展类的接口
      interface NewClass { count: number }

      // 定义类装饰器
      function CountNewInstances(target: NewClassConstructor, context: ClassDecoratorContext) {
      let instanceCount = 0;

      return class extends target {
      constructor(...args: any[]) {
      super(...args);
      instanceCount++;
      this.count = instanceCount;
      }
      }
      }

      // 使用类装饰器
      @CountNewInstances
      class NewClass { }

      const instance3 = new NewClass();
      const instance4 = new NewClass();
      console.log(instance3.count, instance4.count); // 1, 2
      console.log(instance3 instanceof NewClass); // true
    • 定义一个类装饰器,返回一个函数,用于替代所装饰类的构造方法,但是禁止该构造方法通过 new 命令新建类的实例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      // 补充:new.target 是 ES6 引入的元属性,用于在构造函数/方法中检测其是否通过 new 关键字调用,如果是 new.target 指向被调用的构造函数/被实例化的类,否则是 undefined
      // 声明构造函数类型
      interface PersonConstructor { new(...args: any[]): Person }

      // 定义类装饰器
      function fucntionCallable(target: PersonConstructor, context: ClassDecoratorContext) {
      return function (...args: any[]) {
      if (!new.target)
      return new target(...args);
      throw new Error('该函数不可以使用 new 命令调用')
      } as unknown as typeof Person;
      }

      // 使用类装饰器
      @fucntionCallable
      class Person {
      constructor(public name: string) { }
      }

      // @ts-ignore 该注释告诉 TypeScript 编译器忽略下一行代码中的类型检查错误
      const jack = Person('jack');
      console.log(jack.name); // 'jack'
    • 定义一个装饰器工厂,其返回的装饰器将所装饰的类注册为自定义的 HTML 元素

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      // 补充-1:customElements.define(name, constructor) 用于将一个 JavaScript 类(通常是继承自 HTMLElement 的类)与一个自定义的 HTML 标签关联起来,从而创建新的 HTML 元素,并定义这些元素的行为和属性。
      // 参数解释
      // - name 字符串,表示自定义元素的名称(必须包含连字符,以区分内置 HTML 元素)
      // - constructor 一个 JavaScript 类,通常是继承自 HTMLElement 的类,定义了自定义元素的行为和属性
      // 注意事项:constrctor 通常继承自 HTMLElement 的类,同时可以在其中定义生命周期回调(如 connectedCallback,用于在元素被插入到文档中时执行特定操作)
      // 补充-2:类装饰器的 context.addInitializer(initializer) 用于注册一段初始化逻辑。接受一个函数作为参数,同时该函数在类的构造函数执行完成后立即执行。
      // 注意事项:context.addInitializer(initializer) 可以多次调用,来注册多个初始化逻辑,这些逻辑会按照注册顺序依次执行

      // 声明构造函数类型
      interface MyComponentConstructor { new(...args: any[]): MyComponent };

      // 定义装饰器工厂
      function customElement(name: string) {
      return function (target: MyComponentConstructor, context: ClassDecoratorContext) {
      context.addInitializer(function(){
      customElements.define(name, target);
      })
      }
      }

      // 使用装饰器工厂
      @customElement('hello-world')
      class MyComponent extends HTMLElement {
      constructor() {
      super();
      }
      connectedCallback() { // 当 <hello-world> 标签被插入到 DOM 时调用
      this.innerHTML = `<h1>Hello World</h1>`
      }
      }

方法装饰器

  1. 方法装饰器的类型定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    type ClassMethodDecorator<T extends Function> = (
    target: T, // 被装饰的类的方法
    context: {
    kind: 'method'; // 标识装饰器应用于方法
    name: string | symbol; // 方法名
    static: boolean; // 所修饰的方法是否为静态方法
    private: boolean; // 所修饰的方法是否为私有方法
    access: { get: () => unknown }; // 访问器对象,用于获取方法的值
    addInitializer(initializer: () => void): void; // 添加方法初始化逻辑的方法,通常在类的构造函数执行期间执行,早于属性的初始化
    }
    ) => T | void;
  2. 使用示例

    • 定义一个方法装饰器,返回一个函数,用于替代所装饰的原始方法

      1
      2
      3
      4
      5
      6
      7
      function replaceMethod(target: Function, context: ClassMethodDecoratorContext) {
      /* 知识回顾:TypeScript 允许函数增添一个名为 this 的参数,位于参数列表的首位,用于描述函数内部 this 的类型。
      在编译时,TypeScript 会对函数进行检查,如果函数参数列表的第一个参数名为 this,那么编译的结果将会删除这个参数。 */
      return function (this: Person) { // 该方法会替换方法装饰器所装饰的方法
      return `How are you, ${this.name}?`;
      }
      }
    • 定义一个方法装饰器,返回一个函数,用于替代所装饰的原始方法,在不改变原始方法功能的前提下,引入日志记录功能

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      function log(target: Function, context: ClassMethodDecoratorContext) {
      return function (this: Person, ...args: any[]) { // 该方法会替换方法装饰器所装饰的方法
      console.log(`LOG: Entering method ${String(context.name)}`);
      const result = target.call(this, ...args);
      console.log(`LOG: Existing method ${String(context.name)}`);
      return result;
      }
      }

      class Person {
      constructor(public name: string) { };

      // 使用方法装饰器
      @replaceMethod
      hello() {
      return `Hi ${this.name}`;
      }

      // 使用方法装饰器
      @log
      greet() {
      return `Hello, my name is ${this.name}`
      }
      }

      const jack = new Person('jack');
      console.log(jack.hello()); // 'How are you, jack?'
      console.log(jack.greet());
      // 'LOG: Entering method greet'
      // 'LOG: Existing method greet'
      // 'Hello, my name is jack'
    • 定义一个装饰器工厂,其返回的方法装饰器可以让所装饰的方法延迟执行

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      function delay(milliseconds: number = 0) {
      return function (target: Function, context: ClassMethodDecoratorContext) {
      if (context.kind === 'method') {
      return function (this: Logger, ...args: any[]) { // 该方法会替换方法装饰器所装饰的方法
      setTimeout(() => {
      target.call(this, ...args);
      }, milliseconds)
      }
      }
      }
      }

      class Logger {
      @delay(1000)
      log(msg: string) {
      console.log(msg);
      }
      }

      const logger = new Logger();
      logger.log('Hello World');
    • 定义一个方法装饰器,通过 addInitializer() 添加初始化逻辑,将方法的 this 绑定到新创建的实例,从而确保方法在任何上下文中调用时,this 始终指向类的实例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      // 补充:context.addInitializer(initializer) 注册的初始化逻辑在构造函数执行期间执行
      // 注意事项:与类装饰器类似,方法装饰器的 context.addInitializer(initializer) 也可以多次调用,用来注册多个初始化逻辑,并按照注册顺序依次执行
      function Bound(target: Function, context: ClassMethodDecoratorContext) {
      const methodName = context.name as string;

      if (context.private)
      throw new Error(`不能给私有方法 ${methodName} 绑定 this`);

      context.addInitializer(function (this: any) {
      this[methodName] = target.bind(this);
      })
      }

      class Animal {
      constructor(public name: string) { };

      @Bound
      eat(food: string) {
      console.log(`${this.name} is eating ${food}`);
      }
      }

      const eat = new Animal("Lucky").eat;
      eat('meat'); // 'Lucky is eating meat'
    • 定义一个方法装饰器,通过 addInitializer() 添加初始化逻辑,将所修饰的方法名放入一个 Set 保存,并将这个 Set 作为实例属性 collectedMethodKeys

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      function collect(target: Function, context: ClassMethodDecoratorContext) {
      context.addInitializer(function (this: any) {
      if (!this.collectedMethodKeys)
      this.collectedMethodKeys = new Set();
      this.collectedMethodKeys.add(context.name as string);
      })
      }

      interface Car { collectedMethodKeys: Set<string> };

      class Car {
      @collect
      wheel() { }

      @collect
      charge() { }

      @collect
      upkeep() { }
      }

      const car = new Car();
      console.log(car.collectedMethodKeys); // Set (3) {"wheel", "charge", "upkeep"}

属性装饰器

  1. 属性装饰器的类型定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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; // 属性装饰器可以返回一个函数(会自动执行),参数为所装饰属性的初始值,返回值为该属性的最终值。
  2. 使用示例

    • 定义一个属性装饰器,返回一个函数,用于计算该属性的最终值

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      function twice(_: undefined, context: ClassFieldDecoratorContext) {
      return function (initialValue: number) {
      return initialValue * 2;
      }
      }

      class MyNumber {
      @twice
      pi = 3.14;
      }

      const instance = new MyNumber();
      console.log(instance.pi); // 6.28
    • 定义一个属性装饰器,将 context.accessgettersetter 绑定到全局变量,以便通过该变量访问和修改装饰属性

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      let access;

      function exposeAccess(_: undefined, context: ClassFieldDecoratorContext) {
      access = context.access;
      }

      class Color {
      @exposeAccess
      name = 'green'
      }

      const c = new Color();
      console.log(c.name); // 'green'

      console.log(access!.get(c)); // 'green'

      access!.set(c, 'red');
      console.log(c.name); // 'red'

      /*
      get: function (obj) { return obj.name; }
      set: function (obj, value) { obj.name = value; }
      */

getter、setter 装饰器

  1. getter 装饰器的类型定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    type ClassGetterDecorator = (
    target: Function,
    context: {
    kind: 'getter';
    name: string | symbol;
    static: boolean;
    private: boolean;
    access: { get: () => unknown };
    addInitializer(initializer: () => void): void;
    }
    ) => Function | void; // getter 装饰器可以返回一个函数,取代原来的 getter(取值器)
  2. setter 装饰器的类型定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    type ClassSetterDecorator = (
    target: Function,
    context: {
    kind: 'setter';
    name: string | symbol;
    static: boolean;
    private: boolean;
    access: { set: (value: unknown) => void };
    addInitializer(initializer: () => void): void;
    }
    ) => Function | void; // setter 装饰器可以返回一个函数,取代原来的 setter(存值器)
  3. 使用示例:定义一个 getter 装饰器,返回一个函数,用于取代原来的 getter,同时实现属性的懒加载,即只在首次访问时进行计算,并将结果缓存。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    // 注意:这里涉及知识点 - 属性覆盖,即 Object.defineProperty 在实例上定义了一个新的直接属性,这个属性遮盖了原有的 getter 方法。JavaScript 引擎在查找属性时,会先在对象实例上查找直接属性,然后才会查找原型链上的属性和方法。
    function lazyLoad(target: Function, context: ClassGetterDecoratorContext) {
    return function (this: BigNumbers, ...args: any[]) {
    console.log('lazyloading...');
    const result = target.call(this, ...args);
    Object.defineProperty(this, context.name, {
    value: result,
    writable: false
    })
    return result;
    }
    }

    class BigNumbers {
    @lazyLoad
    get value() {
    console.log('computing...');
    return 'big data'
    }
    }

    const instance = new BigNumbers();
    console.log(instance.hasOwnProperty('value'),'value' in instance); // false true
    console.log(instance.value);
    console.log(instance.hasOwnProperty('value'),'value' in instance); // true true
    console.log(instance.value);
    /*
    false true // value 在原型链上
    lazyloading...
    computing...
    big data
    true true // value 在实例上
    big data
    */
    /*
    补充
    - obj.hasOwnProperty(prop): boolean 检查属性 prop 是否是对象 obj 的实例属性
    - prop in obj: boolean 检查属性 prop 是否是对象 obj 的属性(实例属性和原型链上的属性)
    */

accessor 装饰器

  1. accessor 属性修饰符:使用 accessor 修饰一个属性,等同于为该属性自动生成一对取值器和存值器

    • accessor 修饰符生成的取值器和存值器作用于私有属性,这里的私有属性和公开属性可见下述 #xx

    • accessor 修饰符可以修饰实例属性静态属性私有属性

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      class Demo {
      accessor x = 1;
      }

      // 类似于(上述代码可以看作是下述代码的语法糖)
      class Demo {
      #x = 1;

      get x() {
      return this.#x;
      }

      set x(val) {
      this.#x = val;
      }
      }
  2. accessor 装饰器的类型定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    type ClassAutoAccessorDecorator = (
    target: {
    get: () => unknown;
    set: (value: unknown) => void;
    },
    context: {
    kind: "accessor";
    name: string | symbol;
    access: { get(): unknown, set(value: unknown): void };
    static: boolean;
    private: boolean;
    addInitializer(initializer: () => void): void;
    }
    ) => {
    get?: () => unknown;
    set?: (value: unknown) => void;
    init?: (initialValue: unknown) => unknown;
    } | void; // accessor 装饰器可以返回一个对象,用于替代原有的 get() 和 set() 方法。此外,它还可以包含一个 init() 方法,该方法接受属性在类实例化时的初始值作为参数,并返回该属性的最终初始值。
  3. 使用示例:定义一个 accessor 装饰器,为属性的存值器和取值器添加日志输出功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    function log(target: ClassAccessorDecoratorTarget<Demo, number>, context: ClassAccessorDecoratorContext) {
    const { kind, name } = context;

    if (kind === 'accessor') {
    const { get, set } = target;

    return {
    get() {
    console.log(`getting ${name as string}`);
    return get.call(this as any);
    },
    set(val: number) {
    console.log(`setting ${name as string} to ${val}`);
    return set.call(this as any, val);
    },
    init(initialValue: number) {
    console.log(`initializing ${name as string} with ${initialValue}`);
    return initialValue;
    }
    }
    }

    return;
    }

    class Demo {
    @log accessor value: number = 0;

    constructor(value: number) {
    this.value = value;
    }
    }

    const d = new Demo(124);
    d.value = 125;
    console.log(d.value);
    /*
    "initializing value with 0"
    "setting value to 124"
    "setting value to 125"
    "getting value"
    125
    */

    export { }

装饰器的执行顺序

  1. 装饰器的执行流程:评估阶段 evaluation + 应用阶段 application

    • 评估阶段:计算 @ 符号后面的表达式的值,得到一个函数(按顺序评估

    • 应用阶段:将评估装饰器后得到的函数,应用于所装饰对象(按优先级应用

    • 注意事项

      1. 如果属性名或方法名是计算值,则它们在对应的装饰器评估之后,再进行计算。
      2. 应用阶段的优先级为:静态方法装饰器 -> 原型方法装饰器 -> 静态属性装饰器 -> 实例属性装饰器 -> 类装饰器
      3. 实例属性值在类初始化阶段并不执行,直到类实例化时才执行;静态属性值在类初始化最后执行。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      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('实例属性值');

      @d('静态方法装饰器')
      static fn() { }
      }
      /*
      评估 @d(): 类装饰器
      评估 @d(): 静态属性装饰器
      评估 @d(): 原型方法
      计算方法名
      评估 @d(): 实例属性
      评估 @d(): 静态方法装饰器
      应用 @d(): 静态方法装饰器
      应用 @d(): 原型方法
      应用 @d(): 静态属性装饰器
      应用 @d(): 实例属性
      应用 @d(): 类装饰器
      静态属性值
      */
  2. 使用说明:如果一个方法或属性有多个装饰器,则内层的装饰器先执行,外层的装饰器后执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function logger(target: Function, context: ClassMethodDecoratorContext) {
    console.log('logger 装饰器执行');
    }

    function bound(target: Function, context: ClassMethodDecoratorContext) {
    console.log('bound 装饰器执行');
    }

    class Person {
    name: string;
    constructor(name: string) {
    this.name = name;
    }

    @bound
    @logger
    greet() {
    console.log(`Hello, my name is ${this.name}.`);
    }
    }
    /*
    logger 装饰器执行
    bound 装饰器执行
    */

TS - 装饰器(传统语法)

装饰器语法

类装饰器

方法装饰器

属性装饰器

存取器装饰器

参数装饰器

装饰器的执行顺序

TS - tsconfig.json

  1. 解释:tsconfig.json 是 TypeScript 项目的配置文件,位于项目的根目录。

    如果想用 TypeScript 处理 JavaScript 项目,此时可使用配置文件 jsconfig.json

    tsc 编译器编译 TypeScript 代码时,首先在当前目录搜索,如果不存在则在上一级目录搜索,直到找到为止

    tsc 编译时,可以使用命令行参数 --project-p 来指定配置文件 tsconfig.json 的位置(目录或文件)。

  2. 生成 tsconfig.json

    • 方式一:使用 tsc ,命令自动生成。

      1
      $ tsc --init
    • 方式二:安装 npm 的 @tsconfig 名称空间下的模块,即写好的 tsconfig.json 样本,然后在 tsconfig.json 中使用 extends 关键字引用这个模块。

      1
      $ npm install --save-dev @tsconfig/deno
      1
      2
      3
      {
      "extends": "@tsconfig/deno/tsconfig.json"
      }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
{
// "include": ["src/**/*", "tests/**/*"], /* 数组,指定哪些文件需要编译,即要编译的文件列表。既支持逐一列出文件,也支持通配符。文件位置相对于当前配置文件而定。 */
// /* include属性支持三种通配符:? 指代单个字符;* 指代任意字符,不含路径分隔符;** 指定任意目录层级。 */
// /* 如果不指定文件后缀名,默认包括 .ts、.tsx 和 .d.ts 文件。如果打开了 allowJs,那么还包括 .js 和 .jsx。 */
// "exclude": ["**/*.spec.ts"], /* 数组,指定哪些文件不需要编译,用于从编译列表中去除指定的文件,该属性必须和 include 属性一起使用。 */
// "extends": "@tsconfig/node12/tsconfig.json", /* 指定当前 tsconfig 要继承的配置文件,可以是本地文件,也可以是 npm 模块中的 tsconfig 文件。 */
// /* 编译时,extends 指定的 tsconfig 文件会先加载,然后加载当前的 tsconfig 文件,后者会覆盖前者。 */
// /* 如果一个项目有多个配置,可以把共同的配置写成 tsconfig.base.json,其他的配置文件继承该文件,这样便于维护和修改。 */
// "files": ["a.ts", "b.ts"], /* 数组,指定编译的文件列表,如果其中有一个文件不存在,就会报错。排在前面的文件先编译。 */
// /* 由于该属性需要逐一列出文件,且不支持文件匹配,因此文件较多时,建议使用 include 和 exclude 属性。 */
// "references": [ /* references 属性是一个数组,数组成员为对象,适合一个大项目由许多小项目构成的情况,用来设置需要引用的底层项目。 */
// { "path": "../pkg1" }, /* references 数组成员对象的 path 属性,既可以是含有文件 tsconfig.json 的目录,也可以直接是该文件。 */
// { "path": "../pkg2/tsconfig.json" } /* 与此同时,被引用的底层项目的 tsconfig.json 必须启用 composite 属性。(compilerOptions.composite) */
// ], /* 假设项目 A 引用了项目 B 和 C,那么 B 和 C 编译需要先于 A。 */
//
"compilerOptions": { /* 该属性用于定制编译行为 */
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* 让 TypeScript 项目构建时产生文件 tsbuildinfo,从而完成增量构建。 */
// /* 增量编译是一种编译优化技术,旨在通过只重新编译自上次编译以来发生变化的部分代码来加快编译过程。 */
// /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* 打开某些设置,使得 TypeScript 项目可以进行增量构建,往往跟 incremental 属性配合使用。 */
// /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* 指定编译出来的 JavaScript 代码的 ECMAScript 版本。该配置可取值为 es3、es5、es6/es2015、es2016、es2017、es2018、es2019、es2020、es2021、es2022、esnext */
// /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* 数组,描述项目需要加载的 TypeScript 内置类型描述文件,跟三斜线指令 /// <reference lib="" /> 作用相同。 */
// /* 常见的库文件有, */
// /* - ES5、ES6(也称为 ES2015)、ES2016 等:提供 ECMAScript 不同版本的内置对象和 API 的类型定义。 */
// /* - DOM:提供对浏览器环境中 DOM API 的类型定义支持。 */
// /* - WebWorker:提供对 Web Worker 环境的类型定义支持。 */
// /* - ESNext:提供对最新 ECMAScript 提案的类型定义支持。 */
// /* 如果没有显式指定 lib 选项,TypeScript 会根据 target 选项的值自动选择适当的库文件。 */
// /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* 设置如何处理 .tsx 文件。它可以取以下五个值。 */
// /* - preserve:保持 jsx 语法不变,输出的文件名为.jsx。 */
// /* - react:将 <div /> 编译成 React.createElement("div"),输出的文件名为 .js。 */
// /* - react-native:保持 jsx 语法不变,输出的文件后缀名为 .js。 */
// /* - react-jsx:将 <div /> 编译成 _jsx("div"),输出的文件名为 .js。 */
// /* - react-jsxdev:跟 react-jsx 类似,但是为 _jsx() 加上更多的开发调试项,输出的文件名为 .js。 */
// /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* 这个设置针对的是,在类(class)的顶部声明的属性。TypeScript 早先对这一类属性的处理方法,与写入 ES2022 标准的处理方法不一致。 */
// /* 这个设置设为 true,就用来开启 ES2022 的处理方法,设为 false 就是 TypeScript 原有的处理方法。*/
// /* 它的默认值跟 target 属性有关,如果编译目标是 ES2022 或更高,那么 useDefineForClassFields 默认值为 true,否则为 false。 */
// /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* 指定编译产物的模块格式。它的默认值与 target 属性有关,如果 target 是 ES3 或 ES5,它的默认值是 commonjs,否则就是ES6/ES2015。 */
// /* 可以取以下值:none、commonjs、amd、umd、system、es6/es2015、es2020、es2022、esnext、node16、nodenext。 */
// /* Specify what module code is generated. */
// "rootDir": "./", /* 指定项目的根目录。它告诉编译器从哪个目录开始寻找源文件,并保持该目录的结构在输出目录中。 */
// /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* 确定模块路径的算法,即如何查找模块。它可以取以下四种值。*/
// /* - node:采用 Node.js 的 CommonJS 模块算法。 */
// /* - node16 或 nodenext:采用 Node.js 的 ECMAScript 模块算法,从 TypeScript 4.7 开始支持。 */
// /* - classic:TypeScript 1.6 之前的算法,新项目不建议使用。 */
// /* - bundler:TypeScript 5.0 新增的选项,表示当前代码会被其他打包器(比如 Webpack、Vite、esbuild、Parcel、rollup、swc)处理,从而放宽加载规则,它要求 module 设为 es2015 或更高版本。 */
// /* 它的默认值与 module 属性有关,如果 module 为 AMD、UMD、System 或 ES6/ES2015,默认值为 classic;如果 module 为 node16 或 nodenext,默认值为这两个值;其他情况下,默认值为 Node。 */
// /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* 字符串,指定 TypeScript 项目的基准目录。由于默认是以 tsconfig.json 的位置作为基准目录,所以一般情况不需要使用该属性。 */
// /* 如果设置了基准目录,那么 TypeScript 在 import 模块时,会以基准目录为起点开始查找。 */
// /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* 对象,设置模块名和模块路径的映射,也就是 TypeScript 如何导入 require 或 imports 语句加载的模块。 */
// /* paths 基于 baseUrl 进行加载,所以必须同时设置后者。 */
// /* 如果设置 'baseUrl' 为 './','paths' 为 { "b": ["bar/b"] },那么 require('b') 时,加载的是 ./bar/b */
// /* 如果设置 'baseUrl' 为 './','paths' 为 { "@bar/*": ["bar/*"] },那么 require('@bar/b') 时,加载的是 ./bar/b */
// /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* 将多个目录视为一个虚拟目录,使得这些目录中的文件可以相互引用,就像它们在同一个目录中一样,简化了路径管理。 */
// /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* 设置类型模块所在的目录,默认是 node_modules/@types,该目录里面的模块会自动加入编译。一旦指定了该属性,就不会再用默认值 node_modules/@types 里面的类型模块。 */
// /* 数组,数组的每个成员就是一个目录,它们的路径是相对于 tsconfig.json 位置。 */
// /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* 默认情况下,typeRoots 目录下所有模块都会自动加入编译,如果指定了 types 属性,那么只有其中列出的模块才会自动加入编译。 */
// /* 如果 "types": [],就表示不会自动将所有 @types 模块加入编译。 */
// /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* 数组,指定模块的后缀名。假设该属性取值为 [".ios", ".native", ""],此时 */
// /* TypeScript 对于语句 import * as foo from "./foo";,会搜索以下脚本 ./foo.ios.ts、./foo.native.ts 和 ./foo.ts。 */
// /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "resolveJsonModule": true, /* 允许 import 命令导入 JSON 文件。 */
// /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* 是否允许 TypeScript 项目加载 JS 脚本。编译时,也会将 JS 文件,一起拷贝到输出目录。 */
// /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* 是否对 JS 文件同样进行类型检查。打开这个属性,也会自动打开 allowJs。它等同于在 JS 脚本的头部添加// @ts-check命令。 */
// /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* 设置编译时是否为每个脚本生成类型声明文件.d.ts。 */
// /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* 设置生成 .d.ts 类型声明文件的同时,还会生成对应的 Source Map 文件。 */
// /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* 设置编译后只生成 .d.ts 文件,不生成 .js 文件。 */
// /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* 编译时是否生成 SourceMap 文件。 */
// /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* 设置将 SourceMap 文件写入编译后的 JS 文件中,否则会单独生成一个 .js.map 文件。 */
// /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* 设置是否产生编译结果。如果不生成,TypeScript 编译就纯粹作为类型检查了。 */
// /* Disable emitting files from a compilation. */
// "outFile": "./", /* 设置将所有非模块的全局文件,编译在同一个文件里面。它只有在 module 属性为 None、System、AMD 时才生效,并且不能用来打包 CommonJS 或 ES6 模块。 */
// /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* 指定编译产物的存放目录。如果不指定,编译出来的 .js 文件存放在对应的 .ts 文件的相同位置。 */
/* Specify an output folder for all emitted files. */
// "removeComments": true, /* 移除 TypeScript 脚本里面的注释,默认为 false。 */
// /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* 在 SourceMap 里面设置 TypeScript 源文件的位置。 */
// /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* 指定 SourceMap 文件的位置,而不是默认的生成位置。 */
// /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* 控制是否将源代码内联到生成的 Source Map 文件中。 */
// /* 要求 sourceMap 或 inlineSourceMap 至少打开一个 */
// /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* emitBOM 设置是否在编译结果的文件头添加字节顺序标志 BOM,默认值是false。 */
// /* 字节顺序标记(BOM)是 Unicode 字符编码中的一个特殊字符,位于文本文件的开头,用于指示文件的字节顺序(即字节序)和编码格式。 */
// /* 字节顺序(Byte Order),也称为字节序,是指在计算机内存中存储多字节数据(如整数、浮点数)的字节排列方式。分为大端字节序和小端字节序。 */
// /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* 设置换行符为 CRLF(Windows)还是 LF(Linux)。 */
// /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* 设置在编译结果文件不插入 TypeScript 辅助函数,而是通过外部引入辅助函数来解决,比如 NPM 模块tslib。 */
// /* 当 noEmitHelpers 设置为 true 时,TypeScript 编译器不会在输出的 JavaScript 文件中包含辅助函数。 */
// /* 这意味着如果你的代码使用了需要辅助函数的特性(如类继承、异步函数等),你需要通过其他方式提供这些辅助函数。 */
// /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* 指定一旦编译报错,就不生成编译产物,默认为false。 */
// /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* 将 const enum 结构保留下来,不替换成常量值。 */
// /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* 设置生成的 .d.ts 文件所在的目录。 */
// /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* 设置如果当前 TypeScript 脚本作为单个模块编译,是否会因为缺少其他脚本的类型信息而报错,主要便于非官方的编译工具(比如 Babel)正确编译单个脚本。 */
// /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "allowSyntheticDefaultImports": true, /* 允许 import 命令默认加载没有 default 输出的模块。 */
// /* 打开这个设置,就可以写 import React from "react";,而不是import * as React from "react";。 */
// /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* esModuleInterop 修复了一些 CommonJS 和 ES6 模块之间的兼容性问题。 */
// /* 如果 module 属性为 node16 或 nodenext,则 esModuleInterop 默认为 true,其他情况默认为 false。 */
// /* 不打开这个选项,使用 import * as moment from 'moment' 加载 CommonJS 模块。 */
// /* 打开这个选项,使用 import moment from 'moment' 加载 CommonJS 模块,同时 moment 可以作为一个函数使用。 */
// /* 注意,打开 esModuleInterop,将自动打开 allowSyntheticDefaultImports。 */
// /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* 设置文件名是否为大小写敏感,默认为true。 */
// /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* 打开 TypeScript 的严格检查,默认是关闭的。当该配置打开时,相当于打开了一系列配置, */
// /* alwaysStrict、strictNullChecks、strictBindCallApply、strictFunctionTypes */
// /* strictPropertyInitialization、noImplicitAny、noImplicitThis、useUnknownInCatchVariables */
// /* 打开该选项时,允许关闭 alwaysStrict */
// /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* 设置当一个表达式没有明确的类型描述、且编译器无法推断出具体类型时,是否允许将它推断为 any 类型。 */
// /* 它是一个布尔值,默认为 true,即只要推断出 any 类型就报错。 */
// /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* 是否使用严格类型,如果使用,则禁止变量赋值为 undefined 和 null,除非变量原本就是这两种类型。 */
// /* 它相当于从变量的值里面,排除了 undefined 和 null。 */
// /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* 允许对函数更严格的参数检查。具体来说,如果函数 B 的参数是函数 A 参数的子类型,那么函数 B 不能替代函数 A。 */
// /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* 设置是否对函数的 call()、bind()、apply() 这三个方法进行类型检查。 */
// /* 如果不打开该编译选项,编译器不会对以上三个方法进行类型检查,参数类型都是 any,传入任何参数都不会产生编译错误。 */
// /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* 设置类的实例属性都必须初始化,包括:设置为 undefined 类型,显示初始化,构造函数中赋值。 */
// /* 注意,使用该属性的同时,必须打开 strictNullChecks。 */
// /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* 设置如果 this 被推断为 any 类型是否报错。 */
// /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* 设置 catch 语句捕获的 try 抛出的返回值类型,从 any 变成 unknown。此时,使用 err 之前,必须缩小它的类型,否则会报错。 */
// /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* 确保脚本以 ECMAScript 严格模式进行解析,因此脚本头部不用写 "use strict",默认为 true。 */
// /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* 设置是否允许未使用的局部变量。 */
// /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* 设置是否允许未使用的函数参数。 */
// /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* 设置可选属性不能赋值为 undefined。 */
// /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* 设置是否要求函数任何情况下都必须返回一个值,即函数必须有 return 语句。 */
// /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* 设置是否对没有 break 语句(或者 return 和 throw 语句)的 switch 分支报错,即 case 代码里面必须有终结语句(比如 break)。 */
// /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnreachableCode": true, /* 设置是否允许存在不可能执行到的代码。它的值有三种可能。 */
// /* undefined: 默认值,编辑器显示警告。 */
// /* true:忽略不可能执行到的代码。 */
// /* false:编译器报错。 */
// /* Disable error reporting for unreachable code. */
// "allowUnusedLabels": true, /* 设置是否允许存在没有用到的代码标签(label)。它的值有三种可能。 */
// /* undefined: 默认值,编辑器显示警告。 */
// /* true:忽略没有用到的代码标签。 */
// /* false:编译器报错。 */
// /* Disable error reporting for unused labels. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
// "listEmittedFiles": false, /* 设置编译时在终端显示,生成了哪些文件。 */
// "listFiles": false, /* 设置编译时在终端显示,参与本次编译的文件列表。 */
// "pretty": true, /* 设置美化输出终端的编译信息,默认为 true。 */
// "suppressExcessPropertyErrors": false, /* 关闭对象字面量的多余参数的报错。 */
// "traceResolution": false, /* 控制是否在终端输出模块解析的步骤和决策过程 */
}
}

【纠错或】补充

  1. 类型统中的父子类型兼容
  2. 对象中的 suppressExcessPropertyErrors 编译选项的废除
  3. 对象中的最小兼容属性的表达不规范
本贴参考