• 主页
  • 分类
  • 归档
所有文章 大纲

  • 主页
  • 分类
  • 归档

【ts】TypeScript学习


阅读数:    2021-06-14

简介

TypeScript(简称 TS)是微软公司开发的一种基于 JavaScript (简称 JS)语言的编程语言。

TypeScript 可以看成是 JavaScript 的超集(superset),即它继承了后者的全部语法。

TypeScript 对 JavaScript 添加的最主要部分,就是一个独立的类型系统。

优点:

  • 有利于发现代码错误。
  • 有助于代码重构。
  • 更好的 IDE 支持,做到语法提示和自动补全。

综上所述,TypeScript 有助于提高代码质量,保证代码安全,更适合用在大型的企业级项目。

缺点:

  • 丧失了动态类型的代码灵活性。
  • 增加了编程工作量。
  • 更高的学习成本。
  • 引入了独立的编译步骤。
  • 兼容性问题。(过去大多数的js项目没有typescript适配,需要手动适配)

总的来说,这些缺点使得 TypeScript 不一定适合那些小型的、短期的个人项目。

基本用法

  1. 类型声明

    类型声明的写法,一律为在标识符后面添加“冒号 + 类型”。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // foo类型为string
    let foo:string;
    // 函数参数num类型为number,返回值为string
    function toString(num:number):string {
    return String(num);
    }
    // 报错,TypeScript 规定,变量只有赋值后才能使用
    console.log(foo)
    // 报错,变量的值与声明的类型不一致
    foo = 123;
  2. 类型推断

    类型声明并不是必需的,如果没有,TypeScript 会自己推断类型。

    1
    2
    3
    4
    5
    6
    7
    let foo = 123;
    // 报错,推断类型是number
    foo = 'hello';
    // TypeScript 也可以推断函数的返回值
    function toString(num:number) {
    return String(num);
    }

    从这里可以看到,TypeScript 的设计思想是,类型声明是可选的,你可以加,也可以不加。即使不加类型声明,依然是有效的 TypeScript 代码,只是这时不能保证 TypeScript 会正确推断出类型。

  3. 编译

    JavaScript 的运行环境(浏览器和 Node.js)不认识 TypeScript 代码。所以,TypeScript 项目要想运行,必须先转为 JavaScript 代码,这个代码转换的过程就叫做“编译”(compile)。

    TypeScript 的类型检查只是编译时的类型检查,而不是运行时的类型检查。一旦代码编译为 JavaScript,运行时就不再检查类型了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 安装 TypeScript 官方提供的编译器tsc
    $ npm install -g typescript
    # 检查版本
    $ tsc -v
    Version 5.1.6
    # 编译脚本
    $ tsc app.ts
    # 编译多个脚本
    $ tsc file1.ts file2.ts file3.ts

    tsc常用参数:

    • –outFile:将多个 TypeScript 脚本编译成一个 JavaScript 文件

      1
      $ tsc file1.ts file2.ts --outFile app.js
    • –outDir:指定保存编译结果的目录(默认是当前目录)

      1
      $ tsc app.ts --outDir dist
    • –target:指定编译后的 JavaScript 版本(默认是es3,建议使用es2015)

      1
      $ tsc --target es2015 app.ts
    • –noEmitOnError:一旦报错就停止编译,不生成编译产物

      1
      $ tsc --noEmitOnError app.ts
    • –noEmit: 只检查类型是否正确,不生成 JavaScript 文件

      1
      $ tsc --noEmit app.ts

    详见:https://wangdoc.com/typescript/tsc

    tsconfig.json

    实际工作中并不经常使用命令参数的方式指定编译参数,因为编译参数还是很多的,对于项目而言一般是采用配置文件 tsconfig.json。只要当前目录有这个文件,tsc就会自动读取,所以运行时可以不写参数。

    1
    2
    3
    4
    5
    6
    7
    // tsc file1.ts file2.ts --outFile dist/app.js
    {
    "files": ["file1.ts", "file2.ts"],
    "compilerOptions": {
    "outFile": "dist/app.js"
    }
    }

    详见:https://wangdoc.com/typescript/tsconfig.json

  4. 运行

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 安装
    $ npm install -g ts-node
    # 运行
    $ ts-node app.ts
    # 不带参数会打开一个TypeScript 的 REPL 运行环境
    $ ts-node
    > const twice = (x:string) => x + x;
    > twice('abc')
    'abcabc'

三种特殊类型

any 类型,unknown 类型,never 类型

any 类型

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

1
2
3
4
5
6
let x:any;

// 变量类型一旦设为`any`,TypeScript 实际上会关闭这个变量的类型检查。即使有明显的类型错误,只要句法正确,都不会报错。
x = 1; // 正确
x = 'foo'; // 正确
x = true; // 正确

实际开发中,any 类型主要适用以下两个场合。

(1)出于特殊原因,需要关闭某些变量的类型检查,就可以把该变量的类型设为 any。

(2)为了适配以前老的 JavaScript 项目,让代码快速迁移到 TypeScript,可以把变量类型设为 any。

总之,应该尽量避免使用 any 类型,否则就失去了使用 TypeScript 的意义,变成了戏称的 “AnyScript”

any 类型的两大问题:

(1)类型推断问题

TypeScript 对于无法推断出类型,就会认为该变量的类型是any。这显然是很糟糕的情况,因此 TypeScript 提供了一个编译选项 noImplicitAny,打开该选项,只要赋值的时候推断出any类型就会报错。

1
2
3
# 注意只赋值时推断会出错,如果只是声明不赋值被推断成any不报错。
# 如 let x; var x;
$ tsc --noImplicitAny app.ts

(2)污染问题

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

1
2
3
4
5
6
let x:any = 'hello';
let y:number;

y = x; // 不报错
y * 123 // 不报错
y.toFixed() // 不报错

unknown 类型

为了解决any类型“污染”其他变量的问题,TypeScript 3.0 引入了unknown类型。它与any含义相同,表示类型不确定,可能是任意类型,但是它的使用有一些限制,不像any那样自由,可以视为严格版的any。

  • unknown跟any的相似之处,在于所有类型的值都可以分配给unknown类型。

    1
    2
    3
    4
    5
    let x:unknown;

    x = true; // 正确
    x = 42; // 正确
    x = 'Hello World'; // 正确
  • unknown类型跟any类型的不同之处在于,它不能直接使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 1. unknown类型的变量,不能直接赋值给其他类型的变量(除了any类型和unknown类型)。
    let v:unknown = 123;
    let v1:boolean = v; // 报错
    let v2:number = v; // 报错

    // 2. 不能直接调用unknown类型变量的方法和属性。
    let v1:unknown = { foo: 123 };
    v1.foo // 报错
    let v2:unknown = 'hello';
    v2.trim() // 报错
    let v3:unknown = (n = 0) => n + 1;
    v3() // 报错

    // 3. unknown类型变量能够进行的运算是有限的,只能进行比较运算(运算符==、===、!=、!==、||、&&、?)、取反运算(运算符!)、typeof运算符和instanceof运算符这几种,其他运算都会报错。
    let a:unknown = 1;
    a + 1 // 报错
    a === 1 // 正确

    那么,怎么才能使用unknown类型变量呢?

    答案是只有经过“类型缩小”,unknown类型变量才可以使用。这样设计的目的是,只有明确unknown变量的实际类型,才允许使用它,防止像any那样可以随意乱用,“污染”其他变量。

    1
    2
    3
    4
    let a:unknown = 1;
    if (typeof a === 'number') {
    let r = a + 10; // 正确
    }

    总之,unknown可以看作是更安全的any。一般来说,凡是需要设为any类型的地方,通常都应该优先考虑设为unknown类型。

never 类型

为了保持与集合论的对应关系,以及类型运算的完整性,TypeScript 还引入了“空类型”的概念,即该类型为空,不包含任何值。

由于不存在任何属于“空类型”的值,所以该类型被称为never,即不可能有这样的值。

never 类型的使用场景,主要是在一些类型运算之中,保证类型运算的完整性,详见后面章节。另外,不可能返回值的函数,返回值的类型就可以写成 never,详见《函数》 一章。

1
let x:never;

never类型的一个重要特点是,可以赋值给任意其他类型。

1
2
3
4
5
6
7
function f():never {
throw new Error('Error');
}

let v1:number = f(); // 不报错
let v2:string = f(); // 不报错
let v3:boolean = f(); // 不报错

由于该类型的特殊性,平时用的极少,主要是杜绝后面使用该变量。

总之,TypeScript 有两个“顶层类型”(any和unknown),但是“底层类型”只有never唯一一个。

类型系统

基本类型

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 布尔类型
const x:boolean = true;
// 字符串类型
const x:string = 'hello';
// 数字类型(包含所有整数和浮点数)
const x:number = 123;
// 大整数类型(注意,bigint 类型是 ES2020 标准引入的。如果使用这个类型,TypeScript 编译的目标 JavaScript 版本不能低于 ES2020)
const x:bigint = 123n;
// Symbol 类型
const x:symbol = Symbol(123);
// 对象类型(根据 JavaScript 的设计,object 类型包含了所有对象、数组和函数)(PS: 其实函数这里我是不太理解)
const x:object = { foo: 123 };
const y:object = [1, 2, 3];
const z:object = (n:number) => n + 1;
// undefined和null类型(注意,如果没有声明类型的变量,被赋值为undefined或null,在关闭编译设置noImplicitAny和strictNullChecks时,它们的类型会被推断为any)
const x:undefined = undefined;
const x:null = null;

包装对象类型

JavaScript 的8种类型之中,undefined和null其实是两个特殊值,object属于复合类型,剩下的五种属于原始类型(primitive value),代表最基本的、不可再分的值。

  • boolean
  • string
  • number
  • bigint
  • symbol

上面这五种原始类型的值,都有对应的包装对象(wrapper object)。所谓“包装对象”,指的是这些值在需要时,会自动产生的对象(详见:包装对象类型) 。

1
2
3
4
// s就是字符串hello的包装对象
const s = new String('hello');
typeof s // 'object'
s.charAt(1) // 'e'

为了区分字面量和包装对象,TypeScript 对五种原始类型分别提供了大写和小写两种类型。

  • Boolean 和 boolean
  • String 和 string
  • Number 和 number
  • BigInt 和 bigint
  • Symbol 和 symbol

其中,大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量,不包含包装对象。

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'); // 报错

注意,Symbol()和BigInt()这两个函数不能当作构造函数使用,所以没有办法直接获得 symbol 类型和 bigint 类型的包装对象. 目前在 TypeScript 里面,symbol和Symbol两种写法没有差异,bigint和BigInt也是如此,不知道是否属于官方的疏忽。

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

Object 和 object

TypeScript 的对象类型也有大写Object和小写object两种。

大写的Object

大写的Object类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值,都是Object类型,这囊括了几乎所有的值。

1
2
3
4
5
6
7
8
let obj:Object;

obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
  1. 事实上,除了undefined和null这两个值不能转为对象,其他任何值都可以赋值给Object类型。

  2. 空对象{}是Object类型的简写形式

    1
    2
    let obj:{};
    obj = true;

小写的object

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

1
2
3
4
5
6
7
8
let obj:object;

obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
obj = true; // 报错
obj = 'hi'; // 报错
obj = 1; // 报错
  1. 大多数时候,我们使用对象类型,只希望包含真正的对象,不希望包含原始类型。所以,建议总是使用小写类型object,不使用大写类型Object。

  2. 注意,无论是大写的Object类型,还是小写的object类型,都只包含 JavaScript 内置对象原生的属性和方法,用户自定义的属性和方法都不存在于这两个类型之中。如何描述对象的自定义属性,详见《对象类型》一章。

    1
    2
    3
    const o1:Object = { foo: 0 };
    o1.toString() // 正确
    o1.foo // 报错

undefined 和 null

undefined和null既是值,又是类型。

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

1
2
3
4
let age:number = 24;

age = null; // 正确
age = undefined; // 正确

如此就会导致age后面使用number的方法(如toFixed)的时候在编译阶段不报错,在运行阶段就报错了。为了避免这种情况,及早发现错误,TypeScript 提供了一个编译选项strictNullChecks。只要打开这个选项,undefined和null只能赋值给自身,或者any类型和unknown类型的变量。

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

值类型

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

1
2
3
4
let x:'hello';

x = 'hello'; // 正确
x = 'world'; // 报错
  1. TypeScript 推断类型时,遇到const命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // x 的类型是 "https"
    const x = 'https';

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

    // 注意,const命令声明的变量,如果赋值为对象,并不会推断为值类型。
    // x 的类型是 { foo: number }
    const x = { foo: 1 };
  2. 值类型可能会出现一些很奇怪的报错。

    1
    const x:5 = 4 + 1; // 报错

    上面示例中,等号左侧的类型是数值5,等号右侧4 + 1的类型,TypeScript 推测为number。由于5是number的子类型,number是5的父类型,父类型不能赋值给子类型,所以报错了。

    如果一定要让子类型可以赋值为父类型的值,就要用到类型断言。

    1
    const x:5 = (4 + 1) as 5; // 正确

    上面示例中,在4 + 1后面加上as 5,就是告诉编译器,可以把4 + 1的类型视为值类型5,这样就不会报错了。

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

联合类型

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

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

1
2
3
4
let x:string|number;

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

上面示例中,变量x就是联合类型string|number,表示它的值既可以是字符串,也可以是数值。

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

    1
    2
    3
    let setting:true|false;
    let gender:'male'|'female';
    let rainbowColor:'赤'|'橙'|'黄'|'绿'|'青'|'蓝'|'紫';
  2. 联合类型的第一个成员前面,也可以加上竖杠|,这样便于多行书写。

    1
    2
    3
    4
    5
    let x:
    | 'one'
    | 'two'
    | 'three'
    | 'four';
  3. 前面提到,打开编译选项strictNullChecks后,其他类型的变量不能赋值为undefined或null。这时,如果某个变量确实可能包含空值,就可以采用联合类型的写法。

    1
    2
    3
    4
    let name:string|null;

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

    1
    2
    3
    4
    5
    6
    7
    function printId(id:number|string) {
    if (typeof id === 'string') {
    console.log(id.toUpperCase());
    } else {
    console.log(id);
    }
    }

交叉类型

交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号&表示。

交叉类型A&B表示,任何一个类型必须同时属于A和B,才属于交叉类型A&B,即交叉类型同时满足A和B的特征。

1
let x:number&string;

上面示例中,变量x同时是数值和字符串,这当然是不可能的,所以 TypeScript 会认为x的类型实际是never。

交叉类型的主要用途是表示对象的合成,类似属性类型相交。

1
2
3
4
5
6
7
8
let obj:
{ foo: string } &
{ bar: string };

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

类型别名

type命令用来定义一个类型的别名。别名可以让类型的名字变得更有意义,也能增加代码的可读性,还可以使复杂类型用起来更方便,便于以后修改变量的类型。

1
2
type Age = number;
let age:Age = 55;
  1. 别名不允许重名

    1
    2
    type Color = 'red';
    type Color = 'blue'; // 报错
  2. 别名的作用域是块级作用域

    1
    2
    3
    4
    5
    6
    7
    8
    type T = boolean;
    if (true) {
    type T = number;
    let v:T = 5;
    } else {
    type T = string;
    let v:T = 'hello';
    }
  3. 别名支持使用表达式和嵌套

    1
    2
    type World = "world";
    type Greeting = `hello ${World}`;

类型兼容

TypeScript 的类型存在兼容关系,某些类型可以兼容其他类型。

1
2
3
4
type T = number|string;

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

TypeScript 为这种情况定义了一个专门术语。如果类型A的值可以赋值给类型B,那么类型A就称为类型B的子类型(subtype)。在上例中,类型number就是类型number|string的子类型。

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

1
2
3
4
5
let a:'hi' = 'hi';
let b:string = 'hello';

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

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

图片

typeof

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

1
2
3
4
const a = { x: 0 };

type T0 = typeof a; // { x: number }
type T1 = typeof a.x; // number
  1. 要区分 Typescript 和 JavaScript 的 typeof

    1
    2
    3
    4
    5
    6
    7
    8
    let a = 1;
    // typescript 的 typeof, 类型运算,编译后会删除
    let b:typeof a;

    // javascript 的 typeof,值运算,编译后会保留
    if (typeof a === 'number') {
    b = a;
    }
  2. Typescript 的 typeof 参数只能是标识符

    1
    2
    // 编译时不会进行 JavaScript 的值运算
    type T = typeof Date(); // 报错
  3. Typescript 的 typeof 命令参数不能是类型

    1
    2
    type Age = number;
    type MyAge = typeof Age; // 报错
  4. 是“类型缩小”的一种方式

数组类型

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// arr数组成员类型是number
let arr:number[] = [1, 2, 3];
// arr数组成员类型是number或者string
let arr:(number|string)[];

// 第二种写法(泛型)
let arr:Array<number> = [1, 2, 3];
let arr:Array<number|string> = [1, '2', 3];

// 第三种写法(极少用到,了解即可)
interface A {
[prop: number]: string;
}
const arr:A = ['a', 'b', 'c'];
  1. 数组类型声明了以后,成员数量是不限制的,任意数量的成员都可以,也可以是空数组。

    1
    2
    3
    4
    5
    let arr:number[];
    arr = [];
    arr = [1];
    arr = [1, 2];
    arr = [1, 2, 3];
  2. 由于成员数量可以动态变化,所以 TypeScript 不会对数组边界进行检查,越界访问数组并不会报错。

    1
    2
    let arr:number[] = [1, 2, 3];
    let foo = arr[3]; // 正确,类型推导是undefined
  3. 读取数组成员类型

    1
    2
    3
    4
    5
    6
    type Names = string[];

    // 第一种方法:
    type Name = Names[0]; // string
    // 第二种方法(因为数组成员的索引类型都是number):
    type Name = Names[number]; // string
  4. 数组的类型推断

    1
    2
    3
    4
    5
    6
    7
    8
    // 1. 数组初始值为空数组时,类型推断自动更新
    const arr = []; // 推断为 any[]
    arr.push(123); // 推断类型为 number[]
    arr.push('abc'); // 推断类型为 (string|number)[]

    // 2. 数组初始值不为空时,类型推断不会更新
    const arr = [123]; // 推断类型为 number[]
    arr.push('abc'); // 报错
  5. 只读数组

    JavaScript 中,const 命令声明的数组变量是可以改变成员的。

    TypeScript 中,要限定声明数组只读,可以在数组类型前加上 readonly 关键字。

    1
    2
    3
    4
    5
    const arr:readonly number[] = [0, 1];
    // 如果不加 readonly 下面则不报错
    arr[1] = 2; // 报错
    arr.push(3); // 报错
    delete arr[0]; // 报错

    TypeScript 将readonly number[]与number[]视为两种不一样的类型,后者是前者的子类型。

    我们知道,子类型继承了父类型的所有特征,并加上了自己的特征,所以子类型number[]可以用于所有使用父类型的场合,反过来就不行。

    1
    2
    3
    4
    let a1:number[] = [0, 1];
    let a2:readonly number[] = a1; // 正确

    a1 = a2; // 报错

    注意,readonly关键字不能与数组的泛型写法一起使用。

    1
    2
    3
    4
    5
    const arr:readonly Array<number> = [0, 1]; // 报错

    // TypeScript 提供了两个专门的泛型,用来生成只读数组的类型。
    const a1:ReadonlyArray<number> = [0, 1];
    const a2:Readonly<number[]> = [0, 1];

    只读数组还有一种声明方法,就是使用“const 断言”。

    1
    2
    const arr = [0, 1] as const;
    arr[0] = [2]; // 报错
  6. 多维数组

    1
    2
    3
    // 这里是表示一个二维数组
    var multi:number[][] =
    [[1,2,3], [23,24,25]];

元组类型

元组(tuple)是 TypeScript 特有的数据类型,JavaScript 没有单独区分这种类型。它表示成员类型可以自由设置的数组,即数组的各个成员的类型可以不同。

由于成员的类型可以不一样,所以元组必须明确声明每个成员的类型。

1
2
const s:[string, string, boolean]
= ['a', 'b', true];
  1. 类型的自动推断

    使用元组时,必须明确给出类型声明(上例的[number]),不能省略,否则 TypeScript 会把一个值自动推断为数组。

    1
    2
    // a 的类型被推断为 (number | boolean)[]
    let a = [1, true];
  2. 可选成员

    元组成员的类型可以添加问号后缀(?),表示该成员是可选的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let a:[number, number?] = [1];

    // 注意,问号只能用于元组的尾部成员,也就是说,所有可选成员必须在必选成员之后。
    type myTuple = [
    number,
    number,
    number?,
    string?
    ];
  3. 不能越界

    由于需要声明每个成员的类型,所以大多数情况下,元组的成员数量是有限的,从类型声明就可以明确知道,元组包含多少个成员,越界的成员会报错。

    1
    2
    3
    let x:[string, string] = ['a', 'b'];

    x[2] = 'c'; // 报错
  4. 扩展运算符

    使用扩展运算符(...),可以表示不限成员数量的元组。

    1
    2
    3
    4
    5
    6
    7
    type NamedNums = [
    string,
    ...number[]
    ];

    const a:NamedNums = ['A', 1, 2];
    const b:NamedNums = ['B', 1, 2, 3];

    扩展运算符(...)用在元组的任意位置都可以,它的后面只能是一个数组或元组。

    1
    2
    3
    type t1 = [string, number, ...boolean[]];
    type t2 = [string, ...boolean[], number];
    type t3 = [...boolean[], string, number];
  5. 成员名

    元组的成员可以添加成员名,这个成员名是说明性的,可以任意取名,没有实际作用。

    1
    2
    3
    4
    5
    6
    7
    type Color = [
    red: number,
    green: number,
    blue: number
    ];

    const c:Color = [255, 255, 255];
  6. 读取成员类型

    1
    2
    3
    // 读取对应位置成员类型
    type Tuple = [string, number];
    type Age = Tuple[1]; // number

// 读取整个元组的成员类型(由于元组的成员都是数值索引,即索引类型都是number)
type Tuple = [string, number, Date];
type TupleEl = Tuple[number]; // string|number|Date

1
2
3
4
5
6
7
8
9
10
11

7. 只读元组

元组也可以是只读的,不允许修改,有两种写法。

```typescript
// 写法一
type t = readonly [number, string]

// 写法二
type t = Readonly<[number, string]>

跟数组一样,只读元组是元组的父类型。所以,元组可以替代只读元组,而只读元组不能替代元组。

1
2
3
4
5
6
7
type t1 = readonly [number, number];
type t2 = [number, number];

let x:t2 = [1, 2];
let y:t1 = x; // 正确

x = y; // 报错

只读元组的其他写法

1
const arr = [0, 1] as const;

在上一章讲到,生成的是只读数组,其实生成的同时也是只读元组。因为它生成的实际上是一个只读的“值类型”readonly [3, 4],把它解读成只读数组或只读元组都可以。

  1. 成员数量的推断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 1. 如果没有可选成员和扩展运算符,TypeScript 会推断出元组的成员数量(即元组长度)。
    function f(point: [number, number]) {
    if (point.length === 3) { // 报错
    // ...
    }
    }

    // 2. 如果包含了可选成员,TypeScript 会推断出可能的成员数量。
    function f(
    point:[number, number?, number?]
    ) {
    if (point.length === 4) { // 报错
    // ...
    }
    }

// 3. 如果使用了扩展运算符,TypeScript 就无法推断出成员数量。
const myTuple:[…string[]]
= [‘a’, ‘b’, ‘c’];
if (myTuple.length === 4) { // 正确
// …
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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

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

这导致如果函数调用时,使用扩展运算符传入函数参数,可能发生参数数量与数组长度不匹配的报错。

```typescript
const arr = [1, 2];

function add(x:number, y:number){
// ...
}

add(...arr) // 报错

上面示例会报错,原因是函数add()只能接受两个参数,但是传入的是...arr,TypeScript 认为转换后的参数个数是不确定的。

解决这个问题的一个方法,就是把成员数量不确定的数组,写成成员数量确定的元组,再使用扩展运算符。

1
2
3
4
5
6
7
8
9
const arr:[number, number] = [1, 2];
// 或者
// const arr = [1, 2] as const;

function add(x:number, y:number){
// ...
}

add(...arr) // 正确

symbol 类型

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

Symbol 值通过Symbol()函数生成。在 TypeScript 里面,Symbol 的类型使用symbol表示。

1
2
3
4
let x:symbol = Symbol();
let y:symbol = Symbol();

x === y // false

unique symbol

symbol类型包含所有的 Symbol 值,但是无法表示某一个具体的 Symbol 值。

比如,5是一个具体的数值,就用5这个字面量来表示,这也是它的值类型。但是,Symbol 值不存在字面量,必须通过变量来引用,所以写不出只包含单个 Symbol 值的那种值类型。

为了解决这个问题,TypeScript 设计了symbol的一个子类型unique symbol,它表示单个的、某个具体的 Symbol 值。

因为unique symbol表示单个值,所以这个类型的变量是不能修改值的,只能用const命令声明,不能用let声明。

1
2
3
4
5
// 正确
const x:unique symbol = Symbol();

// 报错
let y:unique symbol = Symbol();
  1. unique symbol 声明特定情况下可省略

    const命令为变量赋值 Symbol 值时,变量类型默认就是unique symbol,所以类型可以省略不写。

    1
    2
    3
    const x:unique symbol = Symbol();
    // 等同于
    const x = Symbol();
  2. 每个 unique symbol 类型变量的值不同

    每个声明为unique symbol类型的变量,它们的值都是不一样的,其实属于两个值类型。

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

    a === b // 报错

    上面示例中,变量a和变量b的类型虽然都是unique symbol,但其实是两个值类型。不同类型的值肯定是不相等的,所以最后一行就报错了。

    由于 Symbol 类似于字符串,可以参考下面的例子来理解。

    1
    2
    3
    4
    const a:'hello' = 'hello';
    const b:'world' = 'world';

    a === b // 报错
  3. 每个 unique symbol 类型不能互相赋值

    由于变量a和b是两个类型,就不能把一个赋值给另一个。

    1
    2
    3
    4
    5
    const a:unique symbol = Symbol();
    const b:unique symbol = a; // 报错

    // 要写成与变量a同一个unique symbol值类型的话
    const b:typeof a = a; // 正确
  4. unique symbol 类型是 symbol 类型的子类型

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

    const b:symbol = a; // 正确
    const c:unique symbol = b; // 报错
  5. unique symbol 类型的作用

    • unique symbol 类型的一个作用,就是用作属性名,这可以保证不会跟其他属性名冲突。如果要把某一个特定的 Symbol 值当作属性名,那么它的类型只能是 unique symbol,不能是 symbol。

      1
      2
      3
      4
      5
      6
      7
      const x:unique symbol = Symbol();
      const y:symbol = Symbol();

      interface Foo {
      [x]: string; // 正确
      [y]: string; // 报错
      }
    • unique symbol类型也可以用作类(class)的属性值,但只能赋值给类的readonly static属性。

      1
      2
      3
      4
      class C {
      // 注意,这时static和readonly两个限定符缺一不可,这是为了保证这个属性是固定不变的。
      static readonly foo:unique symbol = Symbol();
      }
  6. 类型推断

    如果变量声明时没有给出类型,TypeScript 会推断某个 Symbol 值变量的类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // let命令声明的变量,推断类型为 symbol。
    let x = Symbol();
    // const命令声明的变量,推断类型为 unique symbol。
    const y = Symbol();

    // const命令声明的变量,如果赋值为另一个 symbol 类型的变量,则推断类型为 symbol。
    const z = x;
    // let命令声明的变量,如果赋值为另一个 unique symbol 类型的变量,则推断类型还是 symbol。
    let t = y;

函数类型

简介

函数的类型声明,需要在声明函数时,给出参数的类型和返回值的类型。

1
2
3
4
5
function hello(
txt:string
):void {
console.log('hello ' + txt);
}

参数类型推断:如果不指定参数类型(比如上例不写txt的类型),TypeScript 就会推断参数类型,如果缺乏足够信息,就会推断该参数的类型为any。

返回值的类型推断:返回值的类型通常可以不写,因为 TypeScript 自己会推断出来。(不过,有时候出于文档目的,或者为了防止不小心改掉返回值,还是会写返回值的类型。)

如果变量被赋值为一个函数,变量的类型有两种写法。

1
2
3
4
5
6
7
8
9
10
11
// 写法一:通过等号右边的函数类型,推断出变量hello的类型;
const hello = function (txt:string) {
console.log('hello ' + txt);
}

// 写法二:使用箭头函数的形式,为变量hello指定类型
const hello:
(txt:string) => void
= function (txt) {
console.log('hello ' + txt);
};

写法二注意事项:

  1. 函数类型里面的参数名与实际参数名,可以不一致。

    1
    2
    3
    4
    5
    6
    let f:(x:number) => number;

    // 这里参数名由x改为y了
    f = function (y:number) {
    return y;
    };
  2. 如果函数的类型定义很冗长,或者多个函数使用同一种类型,写法二用起来就很麻烦。因此,往往用type命令为函数类型定义一个别名,便于指定给其他变量。

    1
    2
    3
    4
    5
    type MyFunc = (txt:string) => void;

    const hello:MyFunc = function (txt) {
    console.log('hello ' + txt);
    };
  3. 函数的实际参数个数,可以少于类型指定的参数个数,但是不能多于,即 TypeScript 允许省略参数。

    1
    2
    3
    4
    5
    6
    7
    8
    let myFunc:
    (a:number, b:number) => number;

    myFunc = (a:number) => a; // 正确

    myFunc = (
    a:number, b:number, c:number
    ) => a + b + c; // 报错

    这是因为 JavaScript 函数在声明时往往有多余的参数,实际使用时可以只传入一部分参数。比如,数组的forEach()方法的参数是一个函数,该函数默认有三个参数(item, index, array) => void,实际上往往只使用第一个参数(item) => void。因此,TypeScript 允许函数传入的参数不足。

  4. 函数参数少的可以赋值给函数参数多的变量

    相当于函数参数少的是子类,多的是父类

    1
    2
    3
    4
    5
    let x = (a:number) => 0;
    let y = (b:number, s:string) => 0;

    y = x; // 正确
    x = y; // 报错
  5. 如果一个变量要套用另一个函数类型,有一个小技巧,就是使用typeof运算符。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function add(
    x:number,
    y:number
    ) {
    return x + y;
    }

    const myAdd:typeof add = function (x, y) {
    return x + y;
    }

    这是一个很有用的技巧,任何需要类型的地方,都可以使用typeof运算符从一个值获取类型。

  6. 函数类型还可以采用对象的写法。

    1
    2
    3
    4
    5
    6
    7
    let add:{
    (x:number, y:number):number
    };

    add = function (x, y) {
    return x + y;
    };

    上面示例中,变量add的类型就写成了一个对象。

    这种写法平时很少用,但是非常合适用在一个场合:函数本身存在属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function f(x:number) {
    console.log(x);
    }
    f.version = '1.0';

    let foo: {
    (x:number): void;
    version: string
    } = f;
  7. 函数类型也可以使用 Interface 来声明,这种写法就是对象写法的翻版

    1
    2
    3
    4
    5
    interface myfn {
    (a:number, b:number): number;
    }

    var add:myfn = (a, b) => a + b;

Function类型

TypeScript 提供 Function 类型表示函数,任何函数都属于这个类型。

1
2
3
function doSomething(f:Function) {
return f(1, 2, 3);
}

Function 类型的函数可以接受任意数量的参数,每个参数的类型都是any,返回值的类型也是any,代表没有任何约束,所以不建议使用这个类型,给出函数详细的类型声明会更好。

箭头函数

箭头函数是普通函数的一种简化写法,它的类型写法与普通函数类似。

1
2
3
4
5
const repeat = (
str: string,
times: number,
fn: (a:string) => void // 注意这里参数类型声明也是箭头函数,但是与函数声明的箭头函数不一样
):void => fn(str.repeat(times));

可选参数

@todo:是否和对象可选属性一样开启 ExactOptionalPropertyTypes和 strictNullChecks,可选属性就不能设为undefined。

如果函数的某个参数可以省略,则在参数名后面加问号表示。

1
2
3
4
5
6
7
function f(x?:number) {
// ...
}

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

参数名带有问号,表示该参数的类型实际上是原始类型|undefined,它有可能为undefined。比如,上例的x虽然类型声明为number,但是实际上是number|undefined。

  1. 函数的可选参数只能在参数列表的尾部,跟在必选参数的后面。

    1
    2
    3
    4
    5
    6
    let myFunc:
    (a?:number, b:number) => number; // 报错

    // 如果前部参数有可能为空,这时只能显式注明该参数类型可能为undefined。
    let myFunc:
    (a?:number|undefined,, b:number) => number; // OK
  2. 函数体内部用到可选参数时,需要判断该参数是否为undefined。

    1
    2
    3
    4
    5
    6
    7
    let myFunc:
    (a:number, b?:number) => number;

    myFunc = function (x, y) {
    if (y === undefined) return x;
    return x + y;
    }

参数默认值

TypeScript 函数的参数默认值写法,与 JavaScript 一致。

设置了默认值的参数,就是可选的。如果不传入该参数,它就会等于默认值。

1
2
3
4
5
6
7
function createPoint(
x = 0, y = 0 // 这里没有写类型,因为 Typescript 可以从参数默认值推导出来是 number
) { // 这里没有写函数返回值类型也是因为 Typescript 可以推导出是 [number, number]
return [x, y];
}

createPoint() // [0, 0]
  1. 可选参数与默认值不能同时使用。

    1
    2
    // 报错
    function f(x?: number = 0) { }
  2. 具有默认值的参数如果不位于参数列表的末尾,调用时不能省略

    1
    2
    3
    4
    5
    6
    function add(x:number = 0, y:number) {
    return x + y;
    }

    add(1) // 报错
    add(undefined, 1) // 正确

参数解构

函数参数如果存在变量解构,类型写法如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function sumArr(
[a, b]: [number, number]
) {
console.log('sumArr', a + b);
}

function sumObj(
{ a, b, c }: {
a: number;
b: number;
c: number
}
) {
console.log('sumObj', a + b + c);
}

sumArr([1, 2])
sumObj({a:1,b:2,c:3})

参数解构可以结合类型别名(type 命令)一起使用,代码会看起来简洁一些。

1
2
3
4
5
type ABC = { a:number; b:number; c:number };

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

reset参数

rest 参数表示函数剩余的所有参数,它可以是数组(剩余参数类型相同),也可能是元组(剩余参数类型不同)。

1
2
3
4
5
6
7
8
9
// rest 参数为数组
function joinNumbers(...nums:number[]) {
// ...
}

// rest 参数为元组
function f(...args:[boolean, number]) {
// ...
}
  1. 元组需要声明每一个剩余参数的类型。如果元组里面的参数是可选的,则要使用可选参数。

    1
    2
    3
    function f(
    ...args: [boolean, string?]
    ) {}
  2. rest 参数甚至可以嵌套。

    1
    2
    3
    function f(...args:[boolean, ...string[]]) {
    // ...
    }
  3. rest 参数可以与变量解构结合使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function repeat(
    ...[str, times]: [string, number]
    ):string {
    return str.repeat(times);
    }

    // 等同于
    function repeat(
    str: string,
    times: number
    ):string {
    return str.repeat(times);
    }

readonly 参数

如果函数内部不能修改某个参数,可以在函数定义时,在参数类型前面加上readonly关键字,表示这是只读参数。

1
2
3
4
5
6
function arraySum(
arr:readonly number[]
) {
// ...
arr[0] = 0; // 报错
}

void 类型

void 类型表示函数没有返回值。

1
2
3
4
function f():void {
console.log('hello');
return 123; // 报错
}
  1. 除了函数,其他变量声明为void类型没有多大意义,因为这时只能赋值为undefined或者null。

    1
    2
    3
    4
    let foo:void = undefined;

    // 没有打开 strictNullChecks 的情况下
    let bar:void = null;
  2. void 类型允许返回undefined或null。

    1
    2
    3
    4
    5
    6
    7
    8
    function f():void {
    return undefined; // 正确
    }

    // 没有打开 strictNullChecks 的情况下
    function f():void {
    return null; // 正确
    }
  3. 如果变量、对象方法、函数参数是一个返回值为 void 类型的函数,该变量、对象方法和函数参数可以接受返回任意值的函数,这时并不会报错(除非后续使用了该函数)。

    1
    2
    3
    4
    5
    6
    7
    type voidFunc = () => void;

    const f:voidFunc = () => {
    return 123; // 正确
    };

    f() * 2 // 报错

    这是因为,这时 TypeScript 认为,这里的 void 类型只是表示该函数的返回值没有利用价值,或者说不应该使用该函数的返回值。只要不用到这里的返回值,就不会报错。(举例来说,数组方法Array.prototype.forEach(fn)的参数fn是一个函数,而且这个函数应该没有返回值,即返回值类型是void。但是,实际应用中,很多时候传入的函数是有返回值,但是它的返回值不重要,或者不产生作用。)

never 类型

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

它主要有以下两种情况。

  1. 抛出错误的函数。

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

    注意,只有抛出错误,才是 never 类型。如果显式用return语句返回一个 Error 对象,返回值就不是 never 类型。

    1
    2
    3
    function fail():Error {
    return new Error("Something failed");
    }
  2. 无限执行的函数。

    1
    2
    3
    4
    5
    const sing = function():never {
    while (true) {
    console.log('sing');
    }
    };

关于never函数的一些说明:

  1. never类型不同于void类型。前者表示函数没有执行结束,不可能有返回值;后者表示函数正常执行结束,但是不返回值,或者说返回undefined。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 正确
    function sing():void {
    console.log('sing');
    }

    // 报错
    function sing():never {
    console.log('sing');
    }
  2. 如果一个函数抛出了异常或者陷入了死循环,那么该函数无法正常返回一个值,因此该函数的返回值类型就是never。如果程序中调用了一个返回值类型为never的函数,那么就意味着程序会在该函数的调用位置终止,永远不会继续执行后续的代码。

  3. 一个函数如果某些条件下有正常返回值,另一些条件下抛出错误,这时它的返回值类型可以省略never。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function sometimesThrow():number {
    if (Math.random() > 0.5) {
    return 100;
    }

    throw new Error('Something went wrong');
    }

    const result = sometimesThrow();

    原因是前面章节提到过,never是 TypeScript 的唯一一个底层类型,所有其他类型都包括了never。从集合论的角度看,number|never等同于number。这也提示我们,函数的返回值无论是什么类型,都可能包含了抛出错误的情况。

高阶函数

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

1
2
const fn = (someValue: number) => (multiplier: number) => someValue * multiplier;
console.log(fn(2)(3))

函数重载

有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)。

1
2
reverse('abc') // 'cba'
reverse([1, 2, 3]) // [3, 2, 1]

上面示例中,函数reverse()可以将参数颠倒输出。参数可以是字符串,也可以是数组。

这意味着,该函数内部有处理字符串和数组的两套逻辑,根据参数类型的不同,分别执行对应的逻辑。这就叫“函数重载”。

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

1
2
3
4
5
6
7
8
9
10
function reverse(str:string):string; // 定义函数重载情况1
function reverse(arr:any[]):any[]; // 定义函数重载情况2
function reverse( // 函数本身类型声明(需要兼容前面的2种情况的重载,即不能情况定义没有number类型,函数声明参数又存在number类型)
stringOrArray:string|any[]
):string|any[] {
if (typeof stringOrArray === 'string')
return stringOrArray.split('').reverse().join('');
else
return stringOrArray.slice().reverse();
}
  1. 重载的各个类型描述与函数的具体实现之间,不能有其他代码,否则报错。

  2. 虽然函数的具体实现里面,有完整的类型声明。但是,函数实际调用的类型,以前面的类型声明为准。比如,上例的函数实现,参数类型和返回值类型都是string|any[],但不意味着参数类型为string时返回值类型为any[]。

  3. 重载声明的排序很重要

    因为 TypeScript 是按照顺序进行检查的,一旦发现符合某个类型声明,就不再往下检查了,所以类型最宽的声明应该放在最后面,防止覆盖其他类型声明。

    1
    2
    3
    4
    5
    6
    7
    function f(x:any):number;
    function f(x:string): 0|1;
    function f(x:any):any {
    // ...
    }

    const a:0|1 = f('hi'); // 报错
  4. 对象的方法也可以使用重载。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class StringBuilder {
    #data = '';

    add(num:number): this;
    add(bool:boolean): this;
    add(str:string): this;
    add(value:any): this {
    this.#data += String(value);
    return this;
    }
    }
  5. 函数重载不仅仅可以通过 同名函数的形式参数(指参数的个数、类型或者顺序)不同,还可通过 返回类型的不同 来重载函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function createElement(
    tag:'a'
    ):HTMLAnchorElement;
    function createElement(
    tag:'canvas'
    ):HTMLCanvasElement;
    function createElement(
    tag:'table'
    ):HTMLTableElement;
    function createElement(
    tag:string
    ):HTMLElement {
    // ...
    }

    上面示例的函数重载,也可以用对象表示。(不过,实际上在 TypeScript 中,我们并不真正写多个重载的函数定义,而是写一个函数实现,具体可参考 interface接口重载一节。)

    1
    2
    3
    4
    5
    6
    type CreateElement = {
    (tag:'a'): HTMLAnchorElement;
    (tag:'canvas'): HTMLCanvasElement;
    (tag:'table'): HTMLTableElement;
    (tag:string): HTMLElement;
    }

总的来说,由于重载是一种比较复杂的类型声明方法,为了降低复杂性,一般来说,如果可以的话,应该优先使用联合类型替代函数重载,除非多个参数之间、或者某个参数与返回值之间,存在对应关系。

构造函数

JavaScript 语言使用构造函数,生成对象的实例。

构造函数的最大特点,就是必须使用new命令调用。

1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
numLegs:number = 4;
}

// 构造函数类型声明
type AnimalConstructor = new () => Animal;

function create(c:AnimalConstructor):Animal {
return new c();
}

const a = create(Animal);
  1. 构造函数还有另一种类型写法,就是采用对象形式。

    1
    2
    3
    type F = {
    new (s:string): object;
    };
  2. 某些函数既是构造函数,又可以当作普通函数使用,比如Date()。这时,类型声明可以写成下面这样。

    1
    2
    3
    4
    type F = {
    new (s:string): object;
    (n?:number): number;
    }

对象类型

简介

除了原始类型,对象是 JavaScript 最基本的数据结构。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
// 写法一:直接使用大括号
const obj:{
x: number, // 属性类型可以用逗号或者分号结尾,建议用逗号
y: number,
add(x:number, y:number): number,
// 或者写成
// add: (x:number, y:number) => number
} = {
x: 1,
y: 1,
add(x, y) {
return x + y;
}
};

// 写法二:把对象类型提炼为一个类型别名
type MyObj = {
x:number;
y:number;
};
const obj:MyObj = { x: 1, y: 1 };

// 写法三:把对象类型提炼为一个接口
interface MyObj {
x: number;
y: number;
}
const obj:MyObj = { x: 1, y: 1 };
  1. 一旦声明了类型,对象赋值时,就不能缺少指定的属性,也不能有多余的属性。

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

    const o1:MyObj = { x: 1 }; // 报错
    const o2:MyObj = { x: 1, y: 1, z: 1 }; // 报错
    const o3:MyObj = { x: 1, y: 1 }; // 正确
  2. 读写不存在的属性也会报错。

    1
    2
    3
    4
    5
    6
    7
    const obj:{
    x:number;
    y:number;
    } = { x: 1, y: 1 };

    console.log(obj.z); // 报错
    obj.z = 1; // 报错
  3. 不能删除类型声明中存在的属性。

    1
    2
    3
    4
    5
    const myUser = { // myUser类型由 typescript 自动推断
    name: "Sabrina",
    };

    delete myUser.name // 报错
  4. 对象类型可以使用方括号读取属性的类型。

    1
    2
    3
    4
    5
    type User = {
    name: string,
    age: number
    };
    type Name = User['name']; // string

可选属性

如果某个属性是可选的(即可以忽略),需要在属性名后面加一个问号。

1
2
3
4
const obj: {
x: number;
y?: number;
} = { x: 1 };
  1. TypeScript 提供编译设置ExactOptionalPropertyTypes,只要同时打开这个设置和strictNullChecks,可选属性就不能设为undefined。

    1
    2
    3
    4
    5
    // 打开 ExactOptionsPropertyTypes 和 strictNullChecks
    const obj: {
    x: number;
    y?: number;
    } = { x: 1, y: undefined }; // 报错
  2. 注意,可选属性与允许设为undefined的必选属性是不等价的。

    1
    2
    3
    4
    5
    type A = { x:number, y?:number };
    type B = { x:number, y:number|undefined };

    const ObjA:A = { x: 1 }; // 正确
    const ObjB:B = { x: 1 }; // 报错
  3. 读取可选属性之前,必须检查一下是否为undefined。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 写法一: 三目运算符
    let firstName = (user.firstName === undefined)
    ? 'Foo' : user.firstName;
    let lastName = (user.lastName === undefined)
    ? 'Bar' : user.lastName;

    // 写法二: 空值合并运算符 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing
    let firstName = user.firstName ?? 'Foo';
    let lastName = user.lastName ?? 'Bar';

只读属性

属性名前面加上readonly关键字,表示这个属性是只读属性,不能修改。

1
2
3
4
5
const person:{
readonly age: number
} = { age: 20 };

person.age = 21; // 报错
  1. 只读属性只能在对象初始化期间赋值,此后就不能修改该属性。

  2. 如果属性值是一个对象,readonly修饰符并不禁止修改该对象的属性,只是禁止完全替换掉该对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    interface Home {
    readonly resident: {
    name: string;
    age: number
    };
    }

    const h:Home = {
    resident: {
    name: 'Vicky',
    age: 42
    }
    };

    h.resident.age = 32; // 正确
    h.resident = {
    name: 'Kate',
    age: 23
    } // 报错
  3. 如果一个对象有两个引用,即两个变量对应同一个对象,其中一个变量是可写的,另一个变量是只读的,那么从可写变量修改属性,会影响到只读变量。

    PS:在 TypeScript 中,当你将一个对象赋值给另一个类型兼容的对象变量时,并不会改变原对象的类型。

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

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

    let w:Person = {
    name: 'Vicky',
    age: 42,
    };

    let r:ReadonlyPerson = w;

    w.age += 1;
    r.age // 43
  4. 如果希望属性值是只读的,除了声明时加上readonly关键字,还有一种方法,就是在赋值时,在对象后面加上只读断言as const。

    1
    2
    3
    4
    5
    const myUser = {
    name: "Sabrina",
    } as const;

    myUser.name = "Cynthia"; // 报错

    注意,上面的as const属于 TypeScript 的类型推断,如果变量明确地声明了类型,那么 TypeScript 会以声明的类型为准。

    1
    2
    3
    4
    5
    const myUser:{ name: string } = {
    name: "Sabrina",
    } as const;

    myUser.name = "Cynthia"; // 正确

属性索引

如果对象的属性非常多,一个个声明类型就很麻烦,而且有些时候,无法事前知道对象会有多少属性,比如外部 API 返回的对象。这时 TypeScript 允许采用属性名表达式的写法来描述类型,称为“属性名的索引类型”。

索引类型里面,最常见的就是属性名的字符串索引。

1
2
3
4
5
6
7
8
9
10
// 不管这个对象有多少属性,只要属性名为字符串,且属性值也是字符串,就符合这个类型声明。
type MyObj = {
[property: string]: string // property表示属性名,这个是可以随便起的
};

const obj:MyObj = {
foo: 'a',
bar: 'b',
baz: 'c',
};
  1. JavaScript 对象的属性名(即上例的property)的类型有三种可能,除了上例的string,还有number和symbol。

    1
    2
    3
    4
    5
    6
    7
    type T1 = {
    [property: number]: string
    };

    type T2 = {
    [property: symbol]: string
    };
  2. 对象可以同时有多种类型的属性名索引,比如同时有数值索引和字符串索引。但是,数值索引不能与字符串索引发生冲突,必须服从后者,这是因为在 JavaScript 语言内部,所有的数值属性名都会自动转为字符串属性名。

    1
    2
    3
    4
    type MyType = {
    [x: number]: boolean; // 报错,只能同为string
    [x: string]: string;
    }
  3. 同样地,可以既声明属性名索引,也声明具体的单个属性名。如果单个属性名不符合属性名索引的范围,两者发生冲突,就会报错。

    1
    2
    3
    4
    type MyType = {
    foo: boolean; // 报错,只能同为string
    [x: string]: string;
    }
  4. 属性的索引类型写法,建议谨慎使用,因为属性名的声明太宽泛,约束太少。另外,属性名的数值索引不宜用来声明数组,因为采用这种方式声明数组,就不能使用各种数组方法以及length属性,因为类型里面没有定义这些东西。

    1
    2
    3
    4
    5
    6
    type MyArr = {
    [n:number]: number;
    };

    const arr:MyArr = [1, 2, 3];
    arr.length // 报错

解构赋值

解构赋值用于直接从对象中提取属性。

1
const {id, name, price} = product;

解构赋值的类型写法,跟为对象声明类型是一样的。

1
2
3
4
5
const {id, name, price}:{
id: string;
name: string;
price: number
} = product;

注意,目前没法为解构变量指定类型,因为对象解构里面的冒号,JavaScript 指定了其他用途,所以不得不写成上面那样,这一点需要特别小心,Typescript中很容易搞混这一点。

1
2
3
4
5
let { x: foo, y: bar } = obj;

// 等同于
let foo = obj.x;
let bar = obj.y;

结构类型原则

只要对象 B 满足 对象 A 的结构特征,TypeScript 就认为对象 B 兼容对象 A 的类型,这称为“结构类型”原则(structural typing)。

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

type B = {
x: number;
y: number;
};

const b:B = {
x: 1,
y: 1
};
const a:A = b; // 正确(注意不能是字面量,会触发严格字面量检查而报错,见下一节)

根据“结构类型”原则,TypeScript 检查某个值是否符合指定类型时,并不是检查这个值的类型名(即“名义类型”),而是检查这个值的结构是否符合要求(即“结构类型”)。

TypeScript 之所以这样设计,是为了符合 JavaScript 的行为。JavaScript 并不关心对象是否严格相似,只要某个对象具有所要求的属性,就可以正确运行。

如果类型 B 可以赋值给类型 A,TypeScript 就认为 B 是 A 的子类型(subtyping),A 是 B 的父类型。子类型满足父类型的所有结构特征,同时还具有自己的特征。凡是可以使用父类型的地方,都可以使用子类型,即子类型兼容父类型。

严格字面量检查

如果对象使用字面量表示,会触发 TypeScript 的严格字面量检查(strict object literal checking)。如果字面量的结构跟类型定义的不一样(比如多出了未定义的属性),就会报错。

1
2
3
4
5
6
7
8
const point:{
x:number;
y:number;
} = { // 如果等号右边不是字面量,而是一个变量,根据结构类型原则,是不会报错的。
x: 1,
y: 1,
z: 1 // 报错
};
  1. TypeScript 对字面量进行严格检查的目的,主要是防止拼写错误。一般来说,字面量大多数来自手写,容易出现拼写错误,或者误用 API。

  2. 由于严格字面量检查,字面量对象传入函数必须很小心,不能有多余的属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    interface Point {
    x: number;
    y: number;
    }

    function computeDistance(point: Point) { /*...*/ }

    computeDistance({ x: 1, y: 2, z: 3 }); // 报错
    computeDistance({x: 1, y: 2}); // 正确

规避严格字面量检查有以下3种方式:

  1. 使用中间变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    type Options = {
    title:string
    };

    let myOptions = {
    title: '我的网页',
    darkmode: true,
    };

    // 根据结构类型原则,任何对象只要有title属性,都认为符合Options类型。
    const obj:Options = myOptions;
  2. 增加索引属性

    1
    2
    3
    4
    5
    6
    let x: {
    foo: number,
    [x: string]: any
    };

    x = { foo: 1, baz: 2 }; // Ok

    如果允许字面量有多余属性,可以像上面这样在类型里面定义一个通用属性。

  3. 使用类型断言

    1
    2
    3
    4
    const obj:Options = {
    title: '我的网页',
    darkmode: true,
    } as Options

    注意上面需要自己确保写的没错,因为类型断言是主动告诉编译器变量的类型,如果不对的话就可能会出现运行时报错

  4. 关闭多余属性检查

    编译器选项suppressExcessPropertyErrors,可以关闭多余属性检查。下面是它在 tsconfig.json 文件里面的写法。

    1
    2
    3
    4
    5
    {
    "compilerOptions": {
    "suppressExcessPropertyErrors": true
    }
    }

最小可选属性规则

根据“结构类型”原则,如果一个对象的所有属性都是可选的,那么其他对象跟它都是结构类似的。

为了避免这种情况,TypeScript 2.4 引入了一个“最小可选属性规则”,也称为“弱类型检测”(weak type detection)。

1
2
3
4
5
6
7
8
9
type Options = {
a?:number;
b?:number;
c?:number;
};

const opts = { d: 123 };

const obj:Options = opts; // 报错

上面示例中,对象opts与类型Options没有共同属性,赋值给该类型的变量就会报错。

报错原因是,如果某个类型的所有属性都是可选的,那么该类型的对象必须至少存在一个可选属性,不能所有可选属性都不存在。这就叫做“最小可选属性规则”。

如果想规避这条规则,要么在类型里面增加一条索引属性([propName: string]: someType),要么使用类型断言(opts as Options)。

空对象

空对象是 TypeScript 的一种特殊值,也是一种特殊类型。

1
2
const obj = {};
obj.prop = 123; // 报错

原因是这时 TypeScript 会推断变量obj的类型为空对象,实际执行的是下面的代码。

1
const obj:{} = {};
  1. 空对象没有自定义属性,所以对自定义属性赋值就会报错。空对象只能使用继承的属性,即继承自原型对象Object.prototype的属性。

    1
    obj.toString() // 正确
  2. 由于 Typescript 中不能动态添加属性,所以对象不能分布生成,必须生成时一次性声明所有属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 错误
    const pt = {};
    pt.x = 3;
    pt.y = 4;

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

    如果确实需要分步声明,一个比较好的方法是,使用扩展运算符(...)合成一个新对象。

    1
    2
    3
    4
    5
    6
    7
    const pt0 = {};
    const pt1 = { x: 3 };
    const pt2 = { y: 4 };

    const pt = {
    ...pt0, ...pt1, ...pt2
    };
  3. 空对象作为类型,其实是Object类型的简写形式。

    1
    2
    3
    4
    5
    6
    7
    8
    let d:{};
    // 等同于
    // let d:Object;

    d = {};
    d = { x: 1 };
    d = 'hello';
    d = 2;

    上面示例中,各种类型的值(除了null和undefined)都可以赋值给空对象类型,跟Object类型的行为是一样的。

    因为Object可以接受各种类型的值,而空对象是Object类型的简写,所以它不会有严格字面量检查,赋值时总是允许多余的属性,只是不能读取这些属性。

    1
    2
    3
    interface Empty { }
    const b:Empty = {myProp: 1, anotherProp: 2}; // 正确
    b.myProp // 报错

    如果想强制使用没有任何属性的对象,可以采用下面的写法。

    1
    2
    3
    4
    5
    6
    interface WithoutProperties {
    [key: string]: never;
    }

    // 报错
    const a:WithoutProperties = { prop: 1 };

interface 接口

简介

interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。使用了某个模板的对象,就拥有了指定的类型结构。

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

const p:Person = {
firstName: 'John',
lastName: 'Smith',
age: 25
};


// 方括号运算符可以取出 interface 某个属性的类型。
type A = Person['age']; // number

interface 可以表示对象的各种语法,它的成员有5种形式。

  • 对象属性
  • 对象的属性索引
  • 对象方法
  • 函数
  • 构造函数
  1. 对象属性

    与对象类型声明类似,有可选属性,只读属性

    1
    2
    3
    4
    5
    interface Point {
    x: number;
    readonly y: number; // 只读
    z?: number; // 可选
    }
  2. 对象的属性索引

    与对象的属性索引一样

    1
    2
    3
    interface A {
    [prop: string]: number;
    }
    • 属性索引共有string、number和symbol三种类型。

    • 一个接口中,最多只能定义一个类型的索引。该类型索引会约束所有该类型的属性。

      1
      2
      3
      4
      5
      interface MyObj {
      [prop: string]: number;

      a: boolean; // 编译错误,a的属性值是布尔,与字符串属性索引定义的不一致
      }
    • 属性的数值索引,其实是指定数组的类型。

      1
      2
      3
      4
      5
      interface A {
      [prop: number]: string;
      }

      const obj:A = ['a', 'b', 'c'];
    • 如果一个 interface 同时定义了字符串索引和数值索引,那么数值索引必须服从于字符串索引。

      因为在 JavaScript 中,数值属性名最终是自动转换成字符串属性名。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      nterface A {
      [prop: string]: number;
      [prop: number]: string; // 报错
      }

      interface B {
      [prop: string]: number;
      [prop: number]: number; // 正确
      }
  3. 对象的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 写法一
    interface A {
    f(x: boolean): string;
    }

    // 写法二
    interface B {
    f: (x: boolean) => string;
    }

    // 写法三
    interface C {
    f: { (x: boolean): string };
    }
    • 属性名可以采用表达式

      1
      2
      3
      4
      5
      6
      7
      8
      9
      const f = 'f';

      interface A {
      [f](x: boolean): string;
      }

      const a:A = {
      f: x => String(x)
      }
    • 类型方法可以重载

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      interface A {
      f(): number;
      f(x: boolean): boolean;
      f(x: string, y: string): string;
      }

      function MyFunc(): number;
      function MyFunc(x: boolean): boolean;
      function MyFunc(x: string, y: string): string;
      function MyFunc(
      x?:boolean|string, y?:string
      ):number|boolean|string {
      if (x === undefined && y === undefined) return 1;
      if (typeof x === 'boolean' && y === undefined) return true;
      if (typeof x === 'string' && typeof y === 'string') return 'hello';
      throw new Error('wrong parameters');
      }

      const a:A = {
      f: MyFunc
      }

      上面示例中,接口A的方法f()有函数重载,需要额外定义一个函数MyFunc()实现这个重载,然后部署接口A的对象a的属性f等于函数MyFunc()就可以了。

  4. 函数

    interface 也可以用来声明独立的函数。

    1
    2
    3
    4
    5
    interface Add {
    (x:number, y:number): number;
    }

    const myAdd:Add = (x,y) => x + y;
  5. 构造函数

    interface 内部可以使用new关键字,表示构造函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Animal {
    numLegs:number = 4;
    }

    // 构造函数类型声明
    // type AnimalConstructor = new () => Animal; // 也可以用type
    interface AnimalConstructor {
    new (): Animal;
    }

    function create(c:AnimalConstructor):Animal {
    return new c();
    }

    const a = create(Animal);

继承

interface 可以继承其他类型,主要有下面几种情况。

  1. 继承 interface

    interface 可以使用extends关键字,继承其他 interface。

    1
    2
    3
    4
    5
    6
    7
    interface Shape {
    name: string;
    }

    interface Circle extends Shape {
    radius: number;
    }

    上面示例中,Circle继承了Shape,所以Circle其实有两个属性name和radius。这时,Circle是子接口,Shape是父接口。

    1. interface 允许多重继承。

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      interface Style {
      color: string;
      }

      interface Shape {
      name: string;
      }

      interface Circle extends Style, Shape {
      radius: number;
      }
    2. 如果子接口与父接口存在同名属性,那么子接口的属性会覆盖父接口的属性。

      注意,子接口与父接口的同名属性必须是类型兼容的,不能有冲突,否则会报错。

      1
      2
      3
      4
      5
      6
      7
      interface Foo {
      id: string;
      }

      interface Bar extends Foo {
      id: number; // 报错
      }
    3. 如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      interface Foo {
      id: string;
      }

      interface Bar {
      id: number;
      }

      // 报错
      interface Baz extends Foo, Bar {
      type: string;
      }
  2. 继承 type

    interface 可以继承type命令定义的对象类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    type Country = {
    name: string;
    capital: string;
    }

    // 和 interface 接口一般,会合并 type 对象属性
    interface CountryWithPop extends Country {
    population: number;
    }

    注意,如果type命令定义的类型不是对象,interface 就无法继承。

  3. 继承 class

    interface 还可以继承 class,即继承该类的所有成员。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class A {
    x:string = '';

    y():boolean {
    return true;
    }
    }

    interface B extends A {
    z: number
    }

    上面示例中,B继承了A,因此B就具有属性x、y()和z。实现B接口的对象就需要实现这些属性。

    某些类拥有私有成员和保护成员,interface 可以继承这样的类,但是意义不大。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class A {
    private x: string = '';
    protected y: string = '';
    }

    interface B extends A {
    z: number
    }

    // 报错
    const b:B = { /* ... */ }

    // 报错
    class C implements B {
    // ...
    }

    上面示例中,A有私有成员和保护成员,B继承了A,但无法用于对象,因为对象不能实现这些成员。这导致B只能用于其他 class,而这时其他 class 与A之间不构成父类和子类的关系,使得x与y无法部署。

合并

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

1
2
3
4
5
6
7
8
interface Box {
height: number;
width: number;
}

interface Box {
length: number;
}

上面示例中,两个Box接口会合并成一个接口,同时有height、width和length三个属性。

这样的设计主要是为了兼容 JavaScript 的行为。JavaScript 开发者常常对全局对象或者外部库,添加自己的属性和方法。那么,只要使用 interface 给出这些自定义属性和方法的类型,就能自动跟原始的 interface 合并,使得扩展外部类型非常方便。

举例来说,Web 网页开发经常会对window对象和document对象添加自定义属性,但是 TypeScript 会报错,因为原始定义没有这些属性。解决方法就是把自定义属性写成 interface,合并进原始定义。

1
2
3
4
5
interface Document {
foo: string;
}

document.foo = 'hello';
  1. 同名接口合并时,同一个属性如果有多个类型声明,彼此不能有类型冲突。

    1
    2
    3
    4
    5
    6
    7
    interface A {
    a: number;
    }

    interface A {
    a: string; // 报错
    }
  2. 同名接口合并时,如果同名方法有不同的类型声明,那么会发生函数重载。而且,后面的定义比前面的定义具有更高的优先级。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    interface Cloner {
    clone(animal: Animal): Animal;
    }

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

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

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

    这个规则有一个例外。同名方法之中,如果有一个参数是字面量类型,字面量类型有更高的优先级。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    interface A {
    f(x:string): void;
    f(x:'foo'): boolean;
    }

    interface A {
    f(x:any): void;
    }

    // 等同于
    interface A {
    f(x:'foo'): boolean;
    f(x:any): void;
    f(x:string): void;
    }
  3. 如果两个 interface 组成的联合类型存在同名属性,那么该属性的类型也是联合类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    interface Circle {
    area: bigint;
    }

    interface Rectangle {
    area: number;
    }

    declare const s: Circle | Rectangle;

    s.area; // bigint | number

    本例中的declare命令表示变量s的具体定义,由其他脚本文件给出,详见《declare 命令》一章。

与type的异同

interface命令与type命令作用类似,都可以表示对象类型。

很多对象类型既可以用 interface 表示,也可以用 type 表示。而且,两者往往可以换用,几乎所有的 interface 命令都可以改写为 type 命令。

它们的相似之处,首先表现在都能为对象类型起名。

1
2
3
4
5
6
7
8
9
type Country = {
name: string;
capital: string;
}

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

上面示例是type命令和interface命令,分别定义同一个类型。

class命令也有类似作用,通过定义一个类,同时定义一个对象类型。但是,它会创造一个值,编译后依然存在。如果只是单纯想要一个类型,应该使用type或interface。

interface 与 type 的区别有下面几点。

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

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

    继承的主要作用是添加属性,type定义的对象类型如果想要添加属性,只能使用&运算符,重新定义一个类型。

    1
    2
    3
    4
    5
    6
    7
    type Animal = {  
    name: string
    }

    type Bear = Animal & {
    honey: boolean // 新增的属性
    }

    作为比较,interface添加属性,采用的是继承的写法。

    1
    2
    3
    4
    5
    6
    7
    interface Animal {
    name: string
    }

    interface Bear extends Animal {
    honey: boolean
    }

    继承时,type 和 interface 是可以换用的。interface 可以继承 type,type 也可以”继承” interface。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    type Foo1 = { x: number; };

    interface Bar extends Foo1 {
    y: number;
    }

    interface Foo2 {
    x: number;
    }

    type Bar = Foo2 & { y: number; };
  3. 同名interface会自动合并,同名type则会报错。

    interface 是开放的,可以添加属性,type 是封闭的,不能添加属性,只能定义新的 type。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    type A = { foo:number }; // 报错
    type A = { bar:number }; // 报错

    interface B { foo:number };
    interface B { bar:number };
    const obj:B = {
    foo: 1,
    bar: 1
    };
  4. interface不能包含属性映射(mapping),type可以,详见《映射》一章。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    interface Point {
    x: number;
    y: number;
    }

    // 正确
    type PointCopy1 = {
    [Key in keyof Point]: Point[Key];
    };

    // 报错
    interface PointCopy2 {
    [Key in keyof Point]: Point[Key];
    };
  5. this关键字只能用于interface。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 报错
    type Foo = {
    add(num:number): this;
    };

    // 正确
    interface Foo {
    add(num:number): this;
    };

    class Calculator implements Foo {
    result = 0;
    add(num:number) {
    this.result += num;
    return this;
    }
    }
  6. type 可以扩展原始数据类型,interface 不行。(@todo: 教程虽然如此写到,但是type的这个有意义吗?找不到应用实例可以满足这个类型声明)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 正确
    type MyStr = string & {
    type: 'new'
    };

    // 报错
    interface MyStr extends string {
    type: 'new'
    }
  7. interface无法表达某些复杂类型(比如交叉类型和联合类型),但是type可以。

    1
    2
    3
    4
    5
    6
    7
    type A = { /* ... */ };
    type B = { /* ... */ };

    type AorB = A | B;
    type AorBwithName = AorB & {
    name: string
    };

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

类

简介

类(class)是面向对象编程的基本构件,封装了属性和方法。

下面关于类的示例都关闭了 “strictPropertyInitialization” 配置,所以很多都是没有赋初值

属性的类型

类的属性可以在顶层声明,也可以在构造方法内部声明。

对于顶层声明的属性,可以在声明时同时给出类型。

1
2
3
4
5
6
7
8
9
10
class Point {
x:number = 1; // PS: 不加类型,typescript也可以自动推断
y:number = 2;
}

// 如果类的顶层属性不赋值而又不报错可以使用 非空断言
class Point {
x!: number; // 如果不给出类型的话,就是any类型
y!: number;
}

PS: TypeScript 有一个配置项strictPropertyInitialization,只要打开(默认是打开的),就会检查属性是否设置了初值,如果没有就报错。

readonly 修饰符

属性名前面加上 readonly 修饰符,就表示该属性是只读的。实例对象不能修改这个属性。

1
2
3
4
5
6
7
8
9
10
class A {
readonly id:string = 'foo'; // 正确

constructor() {
this.id = 'bar'; // 正确
}
}

const a = new A();
a.id = 'bar'; // 报错

上面示例中,构造方法修改只读属性的值也是可以的。或者说,如果两个地方都设置了只读属性的值,以构造方法为准。在其他方法修改只读属性都会报错。

方法的类型

类的方法就是普通函数,类型声明方式与函数一致,可以使用参数默认值,以及函数重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Point {
x:number;
y:number;

// 注意:构造方法不能声明返回值类型,否则报错,因为它总是返回实例对象。
// 参数默认值(存在默认值其实可以不写参数类型)

constructor(x:number = 0, y:number = 0) {
this.x = x; // 顶层声明,构造函数内赋值的情况存在一个细节问题见本节《类的继承》第4点
this.y = y;
}

// 函数重载
add(x:Point);
add(x:number, y: number);
add(x:number|string, y?:string) {
// ...
}
}

注意:构造方法不能声明返回值类型

存取器方法

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
class C {
_name = '';
get name() {
return this._name + this._name;
}
set name(value) {
this._name = value;
}
}

const c = new C();
c.name = 'bar';
console.log(c.name)

TypeScript 对存取器有以下规则

  • 如果某个属性只有get方法,没有set方法,那么该属性自动成为只读属性。

  • TypeScript 5.1 版之前,set方法的参数类型,必须兼容get方法的返回值类型,否则报错。

    TypeScript 5.1 版做出了改变,现在两者可以不兼容。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class C {
    _name = '';
    get name():string {
    return this._name;
    }
    set name(value:number|string) { // TypeScript 5.1 版之前
    this._name = String(value);
    }
    }
  • get方法与set方法的可访问性必须一致,要么都为公开方法,要么都为私有方法。

属性索引

类允许定义属性索引,相当于对象的属性索引

1
2
3
4
5
6
7
8
class MyClass {
[s:string]: boolean |
((s:string) => boolean);

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

PS: 由于类的方法是一种特殊属性(属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型。唯一例外的情况是属性存取器。

1
2
3
4
5
6
7
class MyClass {
[s:string]: boolean;

get isInstance() {
return true;
}
}

上面示例中,属性inInstance的读取器虽然是一个函数方法,但是视同属性,所以属性索引虽然没有涉及方法类型,但是不会报错。

类的interface接口

implements 关键字

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Country {
name:string;
capital:string;
get(name:string): boolean;
}
// 或者
type Country = {
name:string;
capital:string;
get(name:string): boolean;
}

class MyCountry implements Country {
name = '';
capital = '';
get(s:string) {
return true;
}
}
  1. interface 只是指定检查条件,如果不满足这些条件就会报错。它并不能代替 class 自身的类型声明。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    interface A {
    get(name:string): boolean;
    }

    class B implements A {
    get(s) { // s 的类型是 any
    return true;
    }
    }
  2. 与1同理,可选属性在接口中声明之后也是要在类中声明的

  3. implements关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Car {
    id:number = 1;
    move():void {};
    }

    class MyCar implements Car { // 需要实现Car里面的每一个属性和方法
    id = 2; // 不可省略
    move():void {}; // 不可省略
    }
  4. interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。这是因为 TypeScript 设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法

    1
    2
    3
    interface Foo {
    private member:{}; // 报错
    }

实现多个接口

类可以实现多个接口(其实是接受多重限制),每个接口之间使用逗号分隔。

1
2
3
class Car implements MotorVehicle, Flyable, Swimmable {
// ...
}

但是,同时实现多个接口并不是一个好的写法,容易使得代码难以管理,可以使用两种方法替代。

  • 类的继承

    1
    2
    3
    4
    5
    class Car implements MotorVehicle {
    }

    class SecretCar extends Car implements Flyable, Swimmable {
    }
  • 接口的继承

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    interface MotorVehicle {
    // ...
    }
    interface Flyable {
    // ...
    }
    interface Swimmable {
    // ...
    }

    interface SuperCar extends MotoVehicle,Flyable, Swimmable {
    // ...
    }

    class SecretCar implements SuperCar {
    // ...
    }

类与接口的合并

TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
x:number = 1;
}

interface A {
y:number;
}

let a = new A();
console.log(a.y); // undefined
a.y = 10;
console.log(a.x); // 1
console.log(a.y); // 10

Class 类型

实例类型

TypeScript 的类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型。

1
2
3
4
5
6
7
8
9
class Color {
name:string;

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

const green:Color = new Color('green');
  1. 作为类型使用时,类名只能表示实例的类型,不能表示类的自身类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Point {  
    x: number;
    y: number;

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

    function createPoint(
    // PointClass:Point, // 这里的写法就是错误的,因为Point描述的是实例类型,而不是 Class 的自身类型。
    PointClass: typeof Point, // 使用 typeof 获取 Point 类的构造函数类型
    x: number,
    y: number
    ) {
    return new PointClass(x, y);
    }

    const myPoint = createPoint(Point, 10, 20);
    console.log(myPoint); // 输出 Point { x: 10, y: 20 }
    1. 对于引用实例对象的变量来说,既可以声明类型为 Class,也可以声明类型为 Interface,因为两者都代表实例对象的类型。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     interface MotorVehicle {
    }

    class Car implements MotorVehicle {
    }

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

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

  2. 由于类名作为类型使用,实际上代表一个对象,因此可以把类看作为对象类型起名。事实上,TypeScript 有三种方法可以为对象类型起名:type、interface 和 class。

类的自身类型

要获得一个类的自身类型,有2种方法。

  1. typeof 运算符(最简便)

    1
    2
    3
    4
    5
    6
    7
    function createPoint(
    PointClass:typeof Point,
    x:number,
    y:number
    ):Point {
    return new PointClass(x, y);
    }
  2. 构造函数 形式

    JavaScript 语言中,类只是构造函数的一种语法糖,本质上是构造函数的另一种写法。所以,类的自身类型可以写成构造函数的形式。

    1
    2
    3
    4
    5
    6
    7
    function createPoint(
    PointClass: new (x:number, y:number) => Point,
    x: number,
    y: number
    ):Point {
    return new PointClass(x, y);
    }

    构造函数形式的其他写法可以参考《函数类型》一节

结构类型原则

Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo {
id!:number;
}

function fn(arg:Foo) {
// ...
}

const bar = {
id: 10,
amount: 100,
};

fn(bar); // 正确
  1. 如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Person {
    name: string;
    }

    class Customer {
    name: string;
    }

    // 正确
    const cust:Customer = new Person();
  2. 只要 A 类具有 B 类的结构,哪怕还有额外的属性和方法,TypeScript 也认为 A 兼容 B 的类型。

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

    class Customer {
    name: string;
    }

    // 正确
    const cust:Customer = new Person();
    // 报错
    const pers:Person = new Customer();
  3. 不仅是类,如果某个对象跟某个 class 的实例结构相同,TypeScript 也认为两者的类型相同。

    1
    2
    3
    4
    5
    6
    class Person {
    name: string;
    }

    const obj = { name: 'John' };
    const p:Person = obj; // 正确
  4. 凡是类型为空类的地方,所有类(包括对象)都可以使用。(空类不包含任何成员,任何其他类都可以看作与空类结构相同)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Empty {}

    function fn(x:Empty) {
    // ...
    }

    fn({});
    fn(window);
    fn(fn);

    上面示例中,函数fn()的参数是一个空类,这意味着任何对象都可以用作fn()的参数。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Point {
    x: number;
    y: number;
    static t: number;
    constructor(x:number) {}
    }

    class Position {
    x: number;
    y: number;
    z: number;
    constructor(x:string) {}
    }

    const point:Point = new Position(''); // 正确
  6. 如果类中存在私有成员(private)或保护成员(protected),那么确定兼容关系时,TypeScript 要求私有成员和保护成员来自同一个类,这意味着两个类需要存在继承关系。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 情况一
    class A {
    private name = 'a';
    }

    class B extends A {
    }

    const a:A = new B();

    // 情况二
    class A {
    protected name = 'a';
    }

    class B extends A {
    protected name = 'b';
    }

    const a:A = new B();

    上面示例中,A和B都有私有成员(或保护成员)name,这时只有在B继承A的情况下(class B extends A),B才兼容A。

类的继承

类(这里又称“子类”)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法。

1
2
3
4
5
6
7
8
9
10
11
class A {
greet() {
console.log('Hello, world!');
}
}

class B extends A {
}

const b = new B();
b.greet() // "Hello, world!"
  1. 子类可以覆盖基类的同名方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class B extends A {
    greet(name?: string) { // 注意不能与基类的类型定义相冲突,比如 greet(name:string) 就会报错了
    if (name === undefined) {
    super.greet();
    } else {
    console.log(`Hello, ${name}`);
    }
    }
    }
  2. 子类可以更改基类的 保护成员(protected修饰符)为 公开(public修饰符),但是不能改用私有成员(private修饰符)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class A {
    protected x: string = '';
    protected y: string = '';
    protected z: string = '';
    }

    class B extends A {
    // 正确
    public x:string = '';

    // 正确
    protected y:string = '';

    // 报错
    private z: string = '';
    }
  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
    34
    35
    // 例一
    class MyArray extends Array<number> {}

    // 例二
    class MyError extends Error {}

    // 例三
    class A {
    greeting() {
    return 'Hello from A';
    }
    }
    class B {
    greeting() {
    return 'Hello from B';
    }
    }

    interface Greeter {
    greeting(): string;
    }

    interface GreeterConstructor {
    new (): Greeter;
    }

    function getGreeterBase():GreeterConstructor {
    return Math.random() >= 0.5 ? A : B;
    }

    class Test extends getGreeterBase() {
    sayHello() {
    console.log(this.greeting());
    }
    }
  4. 对于那些只设置了类型、没有初值的顶层属性,有一个细节需要注意。

    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 Animal {
    animalStuff: any;
    }

    interface Dog extends Animal {
    dogStuff: any;
    }

    class AnimalHouse {
    resident: Animal;

    constructor(animal:Animal) {
    this.resident = animal;
    }
    }

    class DogHouse extends AnimalHouse {
    resident: Dog;

    constructor(dog:Dog) {
    super(dog);
    }
    }

    const dog = {
    animalStuff: 'animal',
    dogStuff: 'dog'
    };

    const dogHouse = new DogHouse(dog);
    console.log(dogHouse.resident)

    上面示例中,类DogHouse的顶层成员resident只设置了类型(Dog),没有设置初值。这段代码在不同的编译设置下,编译结果不一样。

    如果编译设置的target设成大于等于ES2022,或者useDefineForClassFields设成true,那么DogHouse实例的属性resident输出的是undefined,而不是预料的dog。 原因在于 ES2022 标准的 Class Fields 部分,与早期的 TypeScript 实现不一致,导致子类的那些只设置类型、没有设置初值的顶层成员在基类中被赋值后,会在子类被重置为undefined,详细的解释参见《tsconfig.json》一章,以及官方 3.7 版本的发布说明。

    解决方法就是使用declare命令,去声明顶层成员的类型,告诉 TypeScript 这些成员的赋值由基类实现。

    1
    2
    3
    4
    5
    6
    7
    class DogHouse extends AnimalHouse {
    declare resident: Dog;

    constructor(dog:Dog) {
    super(dog);
    }
    }

可访问性修饰符

类的内部成员的外部可访问性,由三个可访问性修饰符(access modifiers)控制:public、private和protected。

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

public

public修饰符表示这是公开成员,外部可以自由访问。

1
2
3
4
5
6
7
8
class Greeter {
public greet() {
console.log("hi!");
}
}

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

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

正常情况下,除非为了醒目和代码可读性,public都是省略不写的。

private

private修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
private x:number = 0;
}

const a = new A();
a.x // 报错,类的实例不能使用 private 成员

class B extends A {
x = 1; // 报错,子类不能定义父类 private 成员的同名成员
showX() {
console.log(this.x); // 报错,类的子类不能使用 private 成员
}
}
  1. 严格地说,private定义的私有成员,并不是真正意义的私有成员。一方面,编译成 JavaScript 后,private关键字就被剥离了,这时外部访问该成员就不会报错。另一方面,由于前一个原因,TypeScript 对于访问private成员没有严格禁止,使用方括号写法([])或者in运算符,实例对象就能访问该成员。

    1
    2
    3
    const a = new A();
    console.log(a['x']); // 0
    console.log('x' in a)); // true
  2. ES2022 引入了自己的私有成员写法#propName,如果 target 大于或者等于 es2022,建议不使用private,改用 ES2022 的写法,获得真正意义的私有成员。

    1
    2
    3
    4
    5
    6
    class A {
    #x = 1;
    }

    const a = new A();
    a['x'] // 报错
  3. 构造方法也可以是私有的,这就直接防止了使用new命令生成实例对象,只能在类的内部创建实例对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Singleton {
    private static instance?: Singleton;

    private constructor() {}

    static getInstance() {
    if (!Singleton.instance) {
    Singleton.instance = new Singleton();
    }
    return Singleton.instance;
    }
    }

    const s = Singleton.getInstance();

    上面示例使用私有构造方法,实现了单例模式。想要获得 Singleton 的实例,不能使用new命令,只能使用getInstance()方法。

protected

protected修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
protected x = 1;
}

class B extends A {
x = 2
getX() {
return this.x;
}
}

const a = new A();
const b = new B();

console.log(a.x) // 报错,类的实例不能使用 protected 成员
console.log(a.x) // 2
console.log(b.getX()) // 2

实例属性的简写形式

实际开发中,很多实例属性的值,是通过构造方法传入的。

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

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

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

这样的写法等于对同一个属性要声明两次类型,一次在类的头部,另一次在构造方法的参数里面。这有些累赘,TypeScript 就提供了一种简写形式。

1
2
3
4
5
6
7
8
9
10
class Point {
constructor(
public x:number,
public y:number
) {}
}

const p = new Point(10, 10);
p.x // 10
p.y // 10

上面示例中,构造方法的参数x前面有public修饰符,这时 TypeScript 就会自动声明一个公开属性x,不必在构造方法里面写任何代码,同时还会设置x的值为构造方法的参数值。注意,这里的public不能省略。

  1. 除了public修饰符,构造方法的参数名只要有private、protected、readonly修饰符,都会自动声明对应修饰符的实例属性。

  2. readonly还可以与其他三个可访问性修饰符,一起使用。

    1
    2
    3
    4
    5
    6
    7
    class A {
    constructor(
    public readonly x:number,
    protected readonly y:number,
    private readonly z:number
    ) {}
    }

静态成员

类的内部可以使用static关键字,定义静态成员。

静态成员是只能通过类本身使用的成员,不能通过实例对象使用。

1
2
3
4
5
6
7
8
9
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x);
}
}

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

上面示例中,x是静态属性,printX()是静态方法。它们都必须通过MyClass获取,而不能通过实例对象调用。

static关键字前面可以使用 public、private、protected 修饰符,分别对应其应用范围。

范类型

类也可以写成泛型,使用类型参数。关于泛型的详细介绍,请看《泛型》一章。

1
2
3
4
5
6
7
8
9
class Box<Type> {
contents: Type;

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

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

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

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

1
2
3
class Box<Type> {
static defaultContents: Type; // 报错
}

上面示例中,静态属性defaultContents的类型写成类型参数Type会报错。因为这意味着调用时必须给出类型参数(即写成Box<string>.defaultContents),并且类型参数发生变化,这个属性也会跟着变,这并不是好的做法。

抽象类、抽象成员

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

1
2
3
4
5
abstract class A {
id = 1;
}

const a = new A(); // 报错
  1. 抽象类只能当作基类使用,用来在它的基础上定义子类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    abstract class A {
    id = 1;
    }

    class B extends A {
    amount = 100;
    }

    const b = new B();

    b.id // 1
    b.amount // 100
  2. 抽象类的子类也可以是抽象类,也就是说,抽象类可以继承其他抽象类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    abstract class A {
    id = 1;
    }

    class B extends A {
    amount = 100;
    }

    const b = new B();

    b.id // 1
    b.amount // 100
  3. 抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member),即属性名和方法名有abstract关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    abstract class A {
    abstract foo:string;
    abstract execute():string;
    bar:string = '';
    }

    class B extends A {
    foo = 'b';
    execute() {
    return `B executed`;
    }
    }

    这里有几个注意点。

    (1)抽象成员只能存在于抽象类,不能存在于普通类。

    (2)抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加abstract关键字。

    (3)抽象成员前也不能有private修饰符,否则无法在子类中实现该成员。

    (4)一个子类最多只能继承一个抽象类。

总之,抽象类的作用是,确保各种相关的子类都拥有跟基类相同的接口,可以看作是模板。其中的抽象成员都是必须由子类实现的成员,非抽象成员则表示基类已经实现的、由所有子类共享的成员。

this问题

类的方法经常用到this关键字,它表示该方法当前所在的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A {
name = 'A';

getName() {
return this.name;
}
}

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

const b = {
name: 'b',
getName: a.getName
};
b.getName() // 'b'

上面示例中,变量a和b的getName()是同一个方法,但是执行结果不一样,原因就是它们内部的this指向不一样的对象。如果getName()在变量a上运行,this指向a;如果在b上运行,this指向b。

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

1
2
3
4
5
6
7
8
9
10
11
12
class A {
name = 'A';

getName(this: A) {
return this.name;
}
}

const a = new A();
const b = a.getName;

b() // 报错
  1. 编译时,TypeScript 一旦发现函数的第一个参数名为this,则会去除这个参数,即编译结果不会带有该参数。

  2. this参数的类型可以声明为各种对象。

    1
    2
    3
    4
    5
    6
    7
    8
    function foo(
    this: { name: string }
    ) {
    this.name = 'Jack';
    this.name = 0; // 报错
    }

    foo.call({ name: 123 }); // 报错
  3. TypeScript 提供了一个noImplicitThis编译选项。如果打开了这个设置项,如果this的值推断为any类型,就会报错。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // noImplicitThis 打开

    class Rectangle {
    constructor(
    public width:number,
    public height:number
    ) {}

    getAreaFunction() {
    return function () {
    return this.width * this.height; // 报错,因为这里的 this 跟Rectangle这个类没关系,它的类型推断为any
    };
    }
    }
  4. 在类的内部,this本身也可以当作类型使用,表示当前类的实例对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Box {
    static a:this; // 报错,this不允许应用于静态成员,原因是this类型表示实例对象,但是静态成员拿不到实例对象。
    contents:string = '';

    set(value:string):this {
    this.contents = value;
    return this;
    }
    }
  5. 有些方法返回一个布尔值,表示当前的this是否属于某种类型。这时,这些方法的返回值类型可以写成this is Type的形式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class FileSystemObject {
    isFile(): this is FileRep {
    return this instanceof FileRep;
    }

    isDirectory(): this is Directory {
    return this instanceof Directory;
    }

    // ...
    }

    is运算符的介绍详见《类型断言》一章。

泛型

泛型可以理解成一段类型逻辑,需要类型参数来表达。有了类型参数以后,可以在输入类型与输出类型之间,建立一一对应关系。

简介

有些时候,函数返回值的类型与参数类型是相关的。

1
2
3
function getFirst(arr) {
return arr[0];
}

上面示例中,函数getFirst()总是返回参数数组的第一个成员。参数数组是什么类型,返回值就是什么类型。

这个函数的类型声明只能写成下面这样。

1
2
3
function f(arr:any[]):any {
return arr[0];
}

上面的类型声明,就反映不出参数与返回值之间的类型关系。

为了解决这个问题,TypeScript 就引入了“泛型”(generics)。泛型的特点就是带有“类型参数”(type parameter)。

1
2
3
function getFirst<T>(arr:T[]):T {
return arr[0];
}

上面示例中,函数getFirst()的函数名后面尖括号的部分<T>,就是类型参数,参数要放在一对尖括号(<>)里面。本例只有一个类型参数T,可以将其理解为类型声明需要的变量,需要在调用时传入具体的参数类型。

上例的函数getFirst()的参数类型是T[],返回值类型是T,就清楚地表示了两者之间的关系。比如,输入的参数类型是number[],那么 T 的值就是number,因此返回值类型也是number。

函数调用时,需要提供类型参数。

1
getFirst<number>([1, 2, 3])

上面示例中,调用函数getFirst()时,需要在函数名后面使用尖括号,给出类型参数T的值,本例是<number>。

不过为了方便,函数调用时,往往省略不写类型参数的值,让 TypeScript 自己推断。

1
getFirst([1, 2, 3])

上面示例中,TypeScript 会从实际参数[1, 2, 3],推断出类型参数 T 的值为number。

有些复杂的使用场景,TypeScript 可能推断不出类型参数的值,这时就必须显式给出了。

1
2
3
function comb<T>(arr1:T[], arr2:T[]):T[] {
return arr1.concat(arr2);
}

上面示例中,两个参数arr1、arr2和返回值都是同一个类型。如果不给出类型参数的值,下面的调用会报错。

1
comb([1, 2], ['a', 'b']) // 报错

上面示例会报错,TypeScript 认为两个参数不是同一个类型。但是,如果类型参数是一个联合类型,就不会报错。

1
comb<number|string>([1, 2], ['a', 'b']) // 正确

上面示例中,类型参数是一个联合类型,使得两个参数都符合类型参数,就不报错了。这种情况下,类型参数是不能省略不写的。

类型参数的名字,可以随便取,但是必须为合法的标识符。习惯上,类型参数的第一个字符往往采用大写字母。一般会使用T(type 的第一个字母)作为类型参数的名字。如果有多个类型参数,则使用 T 后面的 U、V 等字母命名,各个参数之间使用逗号(“,”)分隔。

扩展,一般按照java泛型的键入参数命名约定,见:https://iowiki.com/java_generics/java_generics_type_parameters.html

  • E - Element,主要由Java Collections框架使用。
  • K - Key,主要用于表示地图键的参数类型。
  • V - Value,主要用于表示地图值的参数类型。
  • N - 数字,主要用于表示数字。
  • T - Type,主要用于表示第一个泛型类型参数。
  • S - Type,主要用于表示第二个泛型类型参数。
  • U - Type,主要用于表示第三个泛型类型参数。
  • V - Type,主要用于表示第四个泛型类型参数。

下面是多个类型参数的例子。

1
2
3
4
5
6
7
8
9
10
11
12
function map<T, U>(
arr:T[],
f:(arg:T) => U
):U[] {
return arr.map(f);
}

// 用法实例
map<string, number>(
['1', '2', '3'],
(n) => parseInt(n)
); // 返回 [1, 2, 3]

上面示例将数组的实例方法map()改写成全局函数,它有两个类型参数T和U。含义是,原始数组的类型为T[],对该数组的每个成员执行一个处理函数f,将类型T转成类型U,那么就会得到一个类型为U[]的数组。

总之,泛型可以理解成一段类型逻辑,需要类型参数来表达。有了类型参数以后,可以在输入类型与输出类型之间,建立一一对应关系。

写法

泛型主要用在四个场合:函数、接口、类和别名。

函数

1
2
3
4
5
6
7
8
9
10
// function关键字定义的泛型函数
function id<T>(arg:T):T {
return arg;
}

// 变量形式定义的泛型函数:写法一
let myId:<T>(arg:T) => T = id;

// 变量形式定义的泛型函数:写法二
let myId:{ <T>(arg:T): T } = id;

接口

1
2
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 Box<Type> {
contents: Type;
}
let box:Box<string>;

// 接口函数的泛型
interface Fn {
<Type>(arg:Type): Type;
}
function id<Type>(arg:Type): Type {
return arg;
}
let myId:Fn = id;

// 接口类的泛型
interface Comparator<T> {
compareTo(value:T): number;
}

class Rectangle implements Comparator<Rectangle> {

compareTo(value:Rectangle): number {
// ...
}
}

类

别名

https://wangdoc.com/typescript/generics

类型参数的默认值

数组泛型的表示

类型参数的约束条件

使用注意点

其他

参考文档:

  • https://wangdoc.com/typescript/intro

  • https://mp.weixin.qq.com/s/NmM8E-t84YSqa6CEamB1GA

  • https://nodejs.cn/typescript/

在线调试:https://ts.nodejs.cn/play

视频教程:https://www.bilibili.com/video/BV1Jv41177bD

官方文档:https://www.tslang.cn/index.html

本文作者: ionluo
本文链接: http://www.ionluo.cn/blog/posts/454fcde8.html
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!
知识共享许可协议
  • 前端
  • typescript

扫一扫,分享到微信

微信分享二维码
【生活】怀旧游戏记录.md
【前端】带链接的弹窗拖拽功能实现
© 2025 ionluo
  • 所有文章
  • 大纲

tag:

  • WEB开发
  • Web后端
  • Django
  • python
  • django
  • web开发
  • 七牛云
  • Docker
  • IDE
  • 前端
  • VSCode
  • Jenkins
  • 运维
  • Linux
  • VirtualBox
  • Centos7
  • NodeJs
  • 转载
  • React
  • Redis
  • Rendertron
  • Web前端
  • SEO
  • Sass
  • Sentry
  • Typora
  • hexo
  • VR
  • docker
  • eslint
  • Git
  • 数据库
  • mysql
  • Nginx
  • nodejs
  • nvm
  • express
  • vue
  • typescript
  • Web安全
  • 工作遇到的问题
  • 随笔
  • 前端 js canvas
  • 业务
  • 面试题
  • 代码规范
  • Css预处理
  • Less
  • svg
  • 前端ssvg
  • 从入门到放弃系列
  • Web代理工具
  • Angular
  • 实战章无法显示问题
  • 兼容问题
  • jquery
  • canvas
  • Grunt
  • angularjs
  • lodash
  • 黑科技
  • JQuery
  • 基础
  • 笔试面试
  • 前端基础
  • 区块链
  • 大前端
  • Centos
  • 设计模式
  • NAS
  • 好文收藏
  • java
  • 微信
  • 公众号
  • 小程序
  • 树莓派
  • 生活
  • 日记
  • 程序人生
  • Windows
  • 通过典型应用快速上手Linux
  • 软考
  • 系统分析师
  • NextJS
  • 系统架构师

    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置:

      jsonContent:
        meta: false
        pages: false
        posts:
          title: true
          date: true
          path: true
          text: false
          raw: false
          content: false
          slug: false
          updated: false
          comments: false
          link: false
          permalink: false
          excerpt: false
          categories: false
          tags: true