前几章都是 js 的基础知识,因此省略从第 5 章开始

第 5 章 TypeScript 类型基础

5.1 类型注解

类型注解的语法由一个冒号 : 和某种具体类型 Type 组成。示例如下

:Type

类型注解总是放在被修饰的实体之后。示例如下

const greeting: string = 'hello, world';

此例中,为常量 greeting 添加了类型注解,将它标记成了 string 类型

类型注解是可选的,编译器大部分情况下都能够自动推断除表达式的类型。示例如下

const greeting = 'hello, world';

此例中,编译器能够从初始值中推断出 greetingstring 类型的常量

类型推断的详细介绍参考 7.3 节

5.2 类型检查

TypeScript 提供两种静态类型检查模式

  • 非严格类型检查(默认方式)
  • 严格类型检查

非严格类型检查

不会对 undefined 值和 null 值做过多限制,允许将它们赋值给 string 类型的变量

严格类型检查

与上面相反

TypeScript 提供了若干个与严格类型检查相关的编译选项,详细介绍参考 8.2 节

tsconfig.json 配置文件中启用严格类型检查。示例如下

{
    "compilerOptions": {
        "strict": true
    }
}

配置文件的详细介绍参考 8.3 节

5.3 原始类型

TypeScript 中的原始类型包含以下几种

  • boolean
  • string
  • number
  • bigint
  • symbol
  • undefined
  • null
  • void
  • 枚举类型
  • 字面量类型

boolean

对应 JavaScript 中的 Boolean 原始类型。该类型能够表示两个逻辑值:true 和 false

boolean 类型使用 boolean 关键字来表示。示例如下

const yes: boolean = true;
const no: boolean = false;

string

对应 JavaScript 中的 String 原始类型。该类型能够表示采用 Unicode UTF-16 编码格式存储的字符序列

string 类型使用 string 关键字表示。示例如下

const foo: string = 'foo';
const bar: string = `bar, ${foo}`;

number

对应 JavaScript 中的 Number 原始类型。该类型能够表示采用双精度 64 位二进制浮点数格式存储的数字

number 类型使用 number 关键字来表示。示例如下

const bin: number = 0b1010;
const oct: number = 0o744;
const integer: number = 10;
const float: number = 3.14;
const hex: number = 0xffffff;

bigint

对应 JavaScript 中的 Bigint 原始类型。该类型能够表示任意精度的整数,但也仅能表示整数。bigint 采用了特殊的对象数据结构来表示和存储一个整数

bigint 类似使用 bigint 关键字来表示。示例如下

const bin: bigint = 0b1010n;
const oct: bigint = 0o744n;
const integer: bigint = 10n;
const hex: bigint = 0xffffffn;

symbol 与 unique symbol

对应 JavaScript 中的 Symbol 原始类型。该类型能够表示任意的 Symbol

symbol 类型使用 symbol 关键字来表示。示例如下

const key: symbol = Symbol();
const symbolHasInstance: symbol = Symbol.hasInstance;

symbol 类型不存在字面量形式,它的值只能通过 Symbol()Symbol.for() 函数来创建或直接引用某个 Well-Known Symbol 值。示例如下

const s0: symbol = Symbol();
const s1: symbol = Symbol.for('foo');
const s2: symbol = Symbol.hasInstance;
const s3: symbol = s0;

TypeScript 引入了 unique symbol 类型,可以将其理解为字面量形式的 symbol。该类型主要用途是用作接口、类等类型中的可计算属性名,因为如果可计算属性的类型名不固定的话,那么该接口将失去意义。示例如下

const x: unique symbol = Symbol();
const y: symbol = Symbol();
interface Foo {
    [x]: string; // 正确
    [y]: string; // 错误
}

只允许使用 constreadonly 属性声明来定义 unique symbol 类型的值,否则将产生错误。示例如下

const a: unique symbol = Symbol();

interface WithUniqueSymbol {
    readonly b: unique symbol;
}

class C {
    static readonly c: unique symbol = Symbol();
}

let a: unique symbol = Symbol(); // 错误

unique symbol 类型的值只允许使用 Symbol() 函数或 Symbol.for() 方法返回值进行初始化,否则将产生错误。示例如下

const a: unique symbol = Symbol();
const b: unique symbol = Symbol('desc');
const c: unique symbol = a; // 错误

使用相同参数调用 Symbol.for() 方法的返回值是相同的,TypeScript 目前无法识别这种情况,因此会编译通过。示例如下

const a: unique symbol = Symbol.for('same');
const b: unique symbol = Symbol.for('same');

每一个 unique symbol 类型都是一种独立的类型,不同 unique symbol 类型之间不允许相互赋值,在比较时永远返回 false。示例如下

const a: unique symbol = Symbol();
const b: unique symbol = Symbol();
console.log(a === b); // false

unique symbolsymbol 的子类型,因此可以将其赋值给 symbol 类型

const a: unique symbol = Symbol();
const b: symbol = a;

如果程序中未使用类型注解,则编译器会自动推断。示例如下

// a 和 b 均为 symbol 类型,因为没有使用 const 类型
let a = Symbol();
let b = Symbol.for('');

// c 和 d 均为 unique symbol 类型
const c = Symbol();
const d = Symbol.for('');

// e 和 f 均为 symbol 类型,没有使用 Symbol() 或 Symbol.for() 初始化
const e = a;
const f = a;

Nullable

Nullable 类型指的是可以为 undefinednull 类型

undefined

只包含一个可能值 undefined

null

只包含一个可能值 null

默认情况下,--strictNullChecks 编译选项没有启用,此时除尾端类型外的所有类型都是 Nullable 类型,也就是所有类型都能够接受 undefinednull 值。尾端类型详细介绍见 5.8 节

开启了此编译选项之后,编译器能够检查出代码中的空值引用错误。此时 undefinednull 能够赋值给顶端类型,同时 undefined 也允许赋值给 void 类型。示例如下

let m1: void = undefined;
let m2: any = undefined;
let m3: unknown = undefined;
let m4: any = null;
let m5: unknown = null;

const foo: undefined = null; // 错误
const bar: null = undefined; // 错误

void

该类型表示某个值不存在,用作函数的返回值类型,除此之外用在其他地方是无意义的。示例如下

function log(message: string): void {
    console.log(message);
}

5.4 枚举类型

枚举类型由 0 个或多个枚举成员构成,每个枚举成员都是一个命名的常量

TypeScript 中,枚举类型是一种原始类型,通过 enum 关键字来定义。示例如下

enum Season {
    Spring,
    Summer,
    Fall,
    Winter,
}

按照枚举成员的类型可以划分为三类

  • 数值型枚举
  • 字符串枚举
  • 异构型枚举

数值型枚举

最常用的枚举类型,是 number 类型的子类型,由一组命名的数值常量构成。如果定义时没有设置枚举成员的值,则会由 0 开始递增自动计算。示例如下

enum Direction {
    Up, // 0
    Down, // 1
    Left, // 2
    Right, // 3
}
const direction: Direction = Direction.Up;

在定义时,可以为一个或多个成员设置初始值,未指定初始值的成员,其值为前一个成员的值加 1。示例如下

enum Direction {
    Up = 1, // 1
    Down, // 2
    Left = 10, // 10
    Right, // 11
}

因为数值型枚举是 number 类型的子类型,因此允许将数值型枚举类型赋值给 number 类型,反之亦然。示例如下

enum Direction {
    Up,
    Down,
    Left,
    Right,
}
const direction: number = Direction.Up;
const d1: Direction = 0;

字符串枚举

string 类型的子类型,字符串枚举成员的值为字符串,必须使用字符串字面量或另一个字符串枚举成员来初始化,其没有自增长行为。示例如下

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

    U = Up,
    D = Down,
    L = Left,
    R = Right,
}
const direction: string = Direction.Up; // 正确,字符串枚举可以赋值给字符串类型
const d1: Direction = 'UP'; // 错误,字符串类型不能赋值给字符串枚举

异构型枚举

允许在一个枚举中同时定义数值型枚举成员和字符串枚举成员,此时称为异构型枚举。实际中不推荐使用,推荐使用对象来代替异构型枚举。示例如下

enum Color {
    Black = 0,
    White = 'White',
}

不允许使用计算的值作为枚举成员的初始值。示例如下

enum Color {
    Black = 0 + 0, // 错误
    White = 'White',
}

紧跟在字符串枚举成员之后的数值型枚举成员需要指定一个初始值。示例如下

enum ColorA {
    Black,
    White = 'White',
}

enum ColorB {
    White = 'White',
    Black, // 错误
}

枚举成员映射

所有类型的枚举,都可以通过枚举成员名去访问值。对于数值型枚举,还可以通过值去获取名。示例如下

enum Bool {
    False = 0,
    True = 1,
}
Bool.False; // 0
Bool.True; // 1

Bool[Bool.False]; // False
Bool[Bool.True]; // True

常量枚举成员与计算枚举成员

根据枚举成员值的当以,可以将枚举成员分为两类

  • 常量枚举成员
  • 计算枚举成员

常量枚举成员

  • 若枚举类型的第一个枚举成员没有定义初始值,则该枚举成员是常量枚举成员并且初始值为 0

  • 若枚举成员没有定义初始值,且紧邻的前一个枚举成员值不是数值型常量,则产生错误

  • 若枚举成员的初始值是常量枚举表达式,则该枚举成员是常量枚举成员。常量枚举表达式是 TypeScript 表达式的子集,它能够在编译阶段被求值,常量枚举表达式的具体规则如下

    • 可以是数字字面量、字符串字面量、不包含替换值的模板字面量
    • 可以是对前面定义的常量枚举成员的引用
    • 可以是用分组运算符包围起来的常量枚举表达式
    • 可以使用医院运算符 +-~,操作数必须为常量枚举表达式
    • 可以使用二元运算符,+-***/%<<>>>>>&|^,两个操作数必须为常量枚举表达式 。示例如下
    enum Foo {
        A = 0, // 数字字面量
        B = 'B', // 字符串字面量
        C = `C`, // 无替换值的模板字面量
        D = A, // 引用前面定义的常量枚举成员
        E = -1, // 一元运算符
        F = 1 + 2, // 二元运算符
        G = (4 / 2) * 3, // 分组运算符(小括号)
    }
    
  • 字面量枚举成员是常量枚举成员的子集,字面量枚举成员是指满足下列条件之一的枚举成员

    • 枚举成员没有定义初始值
    • 枚举成员的初始值为数字字面量、字符串字面量、不包含替换值的模板字面量
    • 枚举成员的初始值为对其他字面量枚举成员的引用

计算枚举成员

除常量枚举成员之外的其他枚举成员都属于计算枚举成员。示例如下

enum Foo {
    A = 'A'.length,
    B = Math.pow(2, 3),
}

使用示例

有时候,程序中并不关注枚举成员值,此时让编译器自动去计算枚举成员值是很方便的。示例如下

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

function move(direction: Direction) {
    switch (direction) {
        case Direction.Up:
            console.log('Up');
            break;
        case Direction.Down:
            console.log('Down');
            break;
        case Direction.Left:
            console.log('Left');
            break;
        case Direction.Right:
            console.log('Right');
            break;
    }
}
move(Direction.Up); // Up
move(Direction.Down); // Down

联合枚举类型

当枚举类型中的所有成员都是字面量枚举成员时,称为联合枚举类型

联合枚举成员类型

联合枚举类型中的枚举成员除了能够表示一个常量值外,还能够表示一种类型,即联合枚举成员类型。示例如下

enum Direction {
    Up,
    Down,
    Left,
    Right,
}
const up: Direction.Up = Direction.Up; // 第一个 Direction.Up 表示类型,第二个表示值

联合枚举成员类型是联合枚举类型的子类型,因此可以将其赋值给联合枚举类型。示例如下

enum Direction {
    Up,
    Down,
    Left,
    Right,
}
const up: Direction.Up = Direction.Up;
const direction: Direction = up;

联合枚举类型

联合枚举类型是由所有联合枚举成员类型构成的联合类型。示例如下

enum Direction {
    Up,
    Down,
    Left,
    Right,
}
type UnionDirectionType =
    | Direction.Up
    | Direction.Down
    | Direction.Left
    | Direction.Right;

Direction 枚举类型是联合枚举类型,它等同于联合类型 UnionDirectionType

由于联合枚举类型是由固定数量的联合枚举成员类型构成的联合类型,因此编译器能够利用该性质对代码进行类型检查。示例如下

enum Direction {
    Up,
    Down,
    Left,
    Right,
}
function f(direction: Direction) {
    if ((direction = Direction.Up)) {
        //
    } else if (direction === Direction.Down) {
        //
    } else if (direction === Direction.Left) {
        //
    } else {
        // 编译器能够分析出此处为 Direction.Right
    }
}

更多子类型兼容性参考 7.1 节

const 枚举类型

const 枚举类型在编译阶段被完全删除,并且在使用了 const 枚举类型的地方会直接将 const 枚举成员的值内联到代码中。示例如下

const enum Direction {
    Up,
    Down,
    Left,
    Right,
}
const directions = [
    Direction.Up,
    Direction.Down,
    Direction.Left,
    Direction.Right,
];
// 编译后生成的内容如下
('use strict');
const direction = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

5.5 字面量类型

将字面量作为类型使用时,称为字面量类型。每个字面量类型只有一个可能的值,即字面量本身

boolean 字面量类型

boolean 类型的子类型,因此可以赋值给 boolean 类型。只有以下两种类型

  • true 字面量
  • false 字面量

string 字面量类型

string 类型的子类型。以下两种字面量都可以作为 string 字面量类型

  • 字符串字面量
  • 不带参数的模板字面量

数字字面量类型

包含以下两种类型

  • number 字面量类型
  • bigint 字面量类型

它们分别是 number 类型和 bigint 类型的子类型

枚举成员字面量类型

联合枚举成员类型也可以称为枚举成员字面量类型,因为联合枚举成员类型使用枚举成员字面量类型表示。示例如下

enum Direction {
    Up,
    Down,
    Left,
    Right,
}
const up: Direction.Up = Direction.Up;
const down: Direction.Down = Direction.Down;
const left: Direction.Left = Direction.Left;
const right: Direction.Right = Direction.Right;

5.6 单元类型

单元类型 Unit Type 也叫做单例类型 Singleton Type,指的是仅包含一个可能值的类型

由于这个特殊的性质,编译器在处理单元类型时,甚至不需要关注单元类型表示的具体值

TypeScript 中单元类型有以下几种

  • undefined 类型
  • null 类型
  • unique symbol 类型
  • void 类型
  • 字面量类型
  • 联合枚举成员类型

5.7 顶端类型

顶端类型 Top Type 源自数学中的类型论,同时它也被广泛应用于计算机编程语言中。它是一种通用类型,在类型系统中,所有类型都是顶端类型的子类型,顶端类型涵盖了类型系统中所有可能的值

TypeScript 中有以下两种

  • any
  • unknown

any

  • 所有类型都是 any 类型的子类型,任何类型的值都可以赋值给 any,也可以将 any 赋值给任何其他类型(除了 never 类型)
  • any 类型上允许执行任意的操作而不会产生编译错误,相当于告诉编译器不要对这个值进行类型检查
  • 尽量减少在代码中使用 any 类型
  • --noImplicityAny 编译选项开启后,会在代码中出现隐式 any 类型转换时提示错误

unknown

  • 任何类型都可以赋值给 unknown 类型
  • unknown 类型只允许赋值给 any 类型和 unknown 类型
  • unknown 类型上不允许执行大多数操作
  • 在使用 unknown 类型时,必须将其细化为某种具体类型。示例如下
    function f(msg: unknown) {
        if (typeof msg === 'string') {
            return msg.length;
        }
    }
    

小结

  • TypeScript 中仅有 anyunknown 两种顶端类型
  • TypeScript 中的所有类型都能够赋值给 anyunknown,相当于两者没有写入的限制
  • any 类型能够赋值给其他任何类型,除了 never 类型
  • unknown 类型仅能够赋值给 any 类型和 unknown 类型
  • 使用 any 类型时,没有任何限制,使用 unknown 类型时,必须将其细化为某种具体类型
  • unknown 类型相当于类型安全的 any 类型,这也是引入 unknown 类型的根本原因
  • 优先考虑使用 unknown 类型

5.8 尾端类型

尾端类型 Bottom Type 是所有其他类型的子类型。因为一个值不可能同时属于所有类型(如不可能同时为数字类型和字符串类型),因此尾端类型中不包含任何值。尾端类型也称为 0 类型或者空类型

TypeScript 中只存在一种尾端类型:never 类型

never

  • never 类型不包含任何可能的值
  • never 类型是所有其他类型的子类型,所以 never 类型允许赋值给任何类型,虽然并不存在 never 类型的值
  • 只有 never 类型自身才能赋值给 never 类型

应用场景

场景一:作为函数的返回值

此时 never 类型表示该函数无法返回一个值(如函数抛出了异常,或者无限循环)。示例如下

function throwError(): never {
    throw new Error();
}
function fail(): never {
    return throwError();
}

function infiniteLoop(): never {
    while (true) {
        //
    }
}

场景二:帮助完成类型计算

在“条件类型”中经常使用 never 类型来帮助完成一些类型计算。示例如下

type Exclude<T, U> = T extends U ? never : T;
type T = Exclude<boolean | string, string>; // boolean

条件类型请参考 6.7 节

场景三:类型推断

在 TypeScript 编译器执行类型推断操作时,如果发现已经没有可用的类型,则推断结果为 never 类型。示例如下

function getLength(msg: string) {
    if (typeof msg === 'string') {
        // string 类型
    } else {
        // never 类型
    }
}

5.9 数组类型

数组值的数据类型为数组类型

数组类型定义

有以下两种方式

  • 简便数组类型表示法:TElement[]
  • 泛型数组类型表示法:Array<TElement>

两种方法在功能上没有任何差别,只是编程风格上不同,由以下三种常见的风格供参考

  • 始终以简便数组类型表示法
  • 始终以泛型数组类型表示法
  • 当数组元素类型为单一原始类型或类型引用时,始终使用简便数组类型表示法,其他情况下不做限制

数组元素类型

在定义了数组类型之后,当访问数组元素时能够获得正确的元素类型信息。示例如下

const digits: number[] = [0, 1, 2];
const zero = digits[0]; // 编译器推断出 zero 为 number 类型
const out: number = digits[100]; // 编译器无法推断是否存在访问越界,因此不会错误

只读数组

  • 只能读取不能操作,也不支持任何能够修改数组元素的方法,如 push()pop()
  • 允许将常规数组类型赋值给只读数组类型,但是不能反过来

有以下三种方式定义一个只读数组,它们只是语法不同,功能上没有任何差别

  • 使用 ReadonlyArray<T> 内置类型:const red: ReadonlyArray<number> = [0, 1, 2]
  • 使用 readonly 修饰符(只允许和简便数组类型表示法一起使用):const red: readonly number[] = [0, 1, 2]
  • 使用 Readonly<T> 工具类型(TypeScript 内置工具类型):const red: Readonly<number[]> = [0, 1, 2]

5.10 元组类型

元组 Tuple 表示由有限元素构成的有序列表。由于元组与数组之间存在很多共性,因此 TypeScript 使用数组来表示元组

在 TypeScript 种,元组类型是数组类型的子类型。元组是长度固定的数组,并且每个元素都有确定的类型

元组的定义

定义元组类型的语法与定义数组字面量的语法相似。示例如下

[T0, T1, ..., Tn]

在给元组类型赋值时,数组中每个元素的类型都要与元组类型的定义保持兼容,且数组长度要一致。使用示例如下

const point: [number, number] = [0, 0];
const score: [string, number] = ['math', 100];

只读元组

允许将常规元组类型赋值给只读元组类型,但是不能反过来

由以下两种方式定义,它们只是语法不同,功能没有任何差别

  • 使用 readonly 修饰符:const point: readonly [number, number] = [0, 0]
  • 使用 Readonly<T> 工具类型:const point: Readonly<[number, number]> = [0, 0]

访问元组中的元素

  • 元组本质上是数组,因此可以使用访问数组元素的方法去访问和修改元组中的元素
  • 当访问元组中不存在的元素时会产生编译错误

元组类型中的可选元素

定义元组时,可以将某些元素定义为可选元素,可选元素必须位于必选元素之后。语法如下

[T0, T1?, ..., Tn?]

使用示例如下

let tuple: [boolean, string?, number?] = [true, 'yes', 1];
tuple = [true];
tuple = [true, 'yes'];
tuple = [true, 'yes', 1];

元组类型中的剩余元素

可以将最后一个元素定义为剩余元素。语法如下

[...T[]]

使用示例如下

let tuple: [number, ...string[]];
tuple = [0];
tuple = [0, 'a'];
tuple = [0, 'a', 'b'];
tuple = [0, 'a', 'b', 'c'];

元组的长度

  • 不包含可选元素和剩余元素是,元组的长度固定
    const tuple: [number, number] = [0, 0];
    tuple.length; // 2
    
  • 包含可选元素时,编译器能够根据元组可选元素的数量识别出元组所有可能的长度,进而构造出一个由数字字面量类型构成的联合类型来表示元组的长度
    const tuple: [boolean, string?, number?] = [true, 'yes', 1];
    tuple.length; // 1 | 2 | 3
    
  • 包含剩余元素时,元组的长度为 number 类型
    const tuple: [boolean, ...string[]] = [true, 'yes'];
    tuple.length; // number
    

元组类型与数组类型的兼容性

  • 元组类型是数组类型的子类型
  • 只读元组类型是只读数组类型的子类型
  • 允许将元组类型赋值给类型兼容的元组类型和数组类型
  • 元组类型允许赋值给常规数组类型和只读数组类型,但是只读元组类型只能赋值给只读数组类型
  • 不允许将数组类型赋值给元组类型

5.11 对象类型

TypeScript 提供了多种定义对象类型的方式,此处先介绍三种基本的对象类型

  • Object 类型
  • object 类型
  • 对象类型字面量

Object

Object 类型表示一种类型,Object() 构造函数表示一个值,它也有自己的类型,但是不是 Object 类型

TypeScript 源码中对 Object() 构造函数的类型定义如下

interface ObjectConstructor {
    readonly prototype: Object;
    //
}
declare var Object: ObjectConstructor;

由此得知

  • Object() 构造函数的类型是 ObjectConstructor 类型
  • Object 类型是特殊对象 Object.prototype 的类型,该类型的主要作用是描述 JavaScript 中几乎所有对象都共享的属性和方法。TypeScript 源码中 Object 类型的具体定义如下
    interface Object {
        constructor: Function;
        toString(): string;
        toLocaleString(): string;
        valueOf(): Object;
        hasOwnProperty(v: PropertyKey): boolean;
        isPrototypeOf(v: Object): boolean;
        propertyIsEnumerable(v: PropertyKey): boolean;
    }
    

类型兼容性

Object 类型有一个特点:除了 undefined 值和 null 值外,其他任何值都可以赋值给 Object 类型

原始值能够赋值给 Object 类型的设计是为了遵循 JavaScript 语言的现有行为

常见错误

该错误为:将 Object 类型应用于自定义变量、参数、属性等属性

前面讲了 Object 类型的作用,因此不应该在此处使用,应该用 object 类型代替

object

  • object 类型的关注点在于类型的分类,它强调一个类型是非原始类型,即对象类型
  • object 类型的关注点不是该对象类型具体包含了哪些属性,因此不允许读取和修改 object 类型上的自定义属性。示例如下
    const obj: object = { foo: 0 };
    obj.foo; // 错误
    obj.foo = 0; // 错误
    
  • object 类型上仅允许访问对象的公共属性和方法,也就是 Object 类型中定义的属性和方法。示例如下
    const obj: object = {};
    obj.toString();
    obj.valueOf();
    

类型兼容性

  • 原始类型不允许赋值给 object 类型
  • object 类型能够赋值给以下三种类型
    • 顶端类型 anyunknown
    • Object 类型
    • 空对象类型字面量 {}

实例应用

对只接受对象作为参数的方法进行约束,如 Object.create()

对象类型字面量

基础语法

示例如下

{
    TypeMember;
    TypeMember;
    ...
}
// 分隔符换成逗号也行
{
    TypeMember,
    TypeMember,
    ...
}

对象类型字面量的类型成员可以分为以下五类

  • 属性签名
  • 调用签名
  • 构造签名
  • 方法签名
  • 索引签名

属性签名

属性签名声明了对象类型中属性成员的名称和类型。语法示例如下

{
    PropertyName: Type;
    PropertyName?: Type;
    readonly PropertyName: Type;
}
  • PropertyName: 表示对象属性名,可以为标识符、字符串、数字、可计算属性名。可计算属性名需要满足以下条件之一
    • 类型为 string 字面量类型或 number 字面量类型
    • 类型为 unique symbol 类型
    • 符合 Symbol.xxx 的形式
  • Type:表示类型。该值可以省略,此时为 any 类型,但不推荐省略
  • ?:表示可选属性。此时该属性类型为 Type | undefined 联合类型
  • readonly:表示只读属性。该属性初始化后就不允许修改

空对象类型字面量

空对象类型字面量 {} 表示不带有任何属性的对象类型,因此不允许在该类型上访问任何自定义属性,但是允许访问对象公共的属性和方法。示例如下

const point: {} = { x: 0, y: 0 };
point.x; // 错误
point.valueOf(); // 正确
  • {}Object 类型单从行为上来看两者是可以互换使用的
  • 两者之间允许互相赋值
  • 两者的区别在于语义不同,{} 相当于 Object 的代理

弱类型

弱类型 Weak Type 指的是同时满足以下条件的对象类型

  • 对象类型中至少包含一个属性
  • 对象类型中所有属性都是可选属性
  • 对象类型中不包含字符串索引签名、数值索引签名、调用签名、构造签名 弱类型示例如下
let config: {
    url?: string;
    async?: boolean;
    timeout?: number;
};

多余属性

多余属性会对类型间关系的判定产生影响,只有在比较两个对象类型的关系时谈论多余属性才有意义

当满足以下条件时,可以认为源对象类型相对于目标对象类型存在多余属性

  • 源对象类型是一个 全新的对象字面量类型
  • 源对象类型中存在一个或多个在目标对象类型中不存在的属性

全新的对象字面量类型 指的是由对象字面量推断出的类型。如图所示

全新的对象字面量类型

上图中,由赋值语句右侧的对象字面量 {x: 0, y: 0} 推断出的类型为全新的对象字面量类型 {x: 0, y: 0}

将代码修改成如下示例

const point: { x: number; y: number } = {
    x: 0,
    y: 0,
    z: 0, // z 是多余属性
};

目标对象类型中的可选属性与必选属性是被同等对待的。所示如下

const point: { x?: number; y?: number } = {
    x: 0,
    y: 0,
    z: 0, // z 是多余属性
};

多余属性检查

多余属性会影响类型间的子类型兼容性以及赋值兼容性,也就是说编译器不允许在一些操作中存在多余属性。如

  • 将对象字面量赋值给变量或属性时
  • 将对象字面量作为函数参数来调用函数时 示例如下
let point: { x: number; y: number } = { x: 0, y: 0, z: 0 }; // 错误,z 是多余属性
function f(point: { x: number; y: number }) {}
f({ x: 0, y: 0, z: 0 }); // 错误,z 是多余属性

多余属性设计意图

  • 在正常的使用场景中,如果直接将一个对象字面量赋值给某个确定类型的变量,那么通常没理由取故意添加多余属性
  • 从类型可靠性的角度看待,当把对象字面量赋值给目标对象类型时,若存在多余属性,那么将意味着对象字面量本身的类型彻底丢失了,因此编译器认为这是一个错误
  • 能够带来的最直接的帮助是发现属性名的拼写错误,编译器还能根据 Levenshtein distance 算法来推测可能的属性名

允许多余属性

  • 使用类型断言(推荐)
  • 启用 --suppressExcessPropertyErrors 编译选项
  • 使用 // @ts-ignore 注释指令
  • 为目标对象类型添加索引签名
  • 先将对象字面量赋值给某个变量,然后再将该变量赋值给目标对象类型,此时将不会执行多余属性检查。该方法的原理与类型断言类似,就是令源对象类型不为 全新的对象字面量类型

5.12 函数类型

常规参数类型

示例如下

function f(x: number, y: number) {
    return x + y;
}

可选参数类型

示例如下

function f(x: number, y?: number) {
    return x + (y ?? 0);
}

默认参数类型

示例如下

function f(x: number = 0, y = 0) {
    return x + y;
}

剩余参数类型

  • 数组类型的剩余参数

    function f(...args: any[]) {}
    
  • 元组类型的剩余参数

    元组类型的剩余参数会被编译器自动展开为独立的形式参数声明。示例如下

    function f(...args: [boolean, number]) {}
    // 等同于
    function f(args_0: boolean, args_1: number) {}
    
    function f1(...args: [boolean, string?]) {}
    // 等同于
    function f1(args_0: boolean, args_1?: string) {}
    
    function f2(...args: [boolean, ...string[]]) {}
    // 等同于
    function f2(args_0: boolean, ...args_1: string[]) {}
    

结构参数类型

示例如下

function f([x, y]: [number, number]) {}
function f({ x, y }: { x: number; y: number }) {}

返回值类型

示例如下

function f(x: number, y: number): number {
    return x + y;
}

大多数情况下编译器能够根据函数体内的 return 语句自动推断出返回值类型,因此也可以省略

void 类型表示一个函数只能返回 undefined 值,如果没有启用 --strictNullChecks 编译选项时,也允许返回 null

函数类型字面量

语法示例如下

(ParameterList) => Type;
  • ParameterList:函数形式参数列表
  • Type:函数返回值类型

使用示例如下

let f: () => void;
f = function () {};

let f: (x: number, y: number) => number;
f = function (x: number, y: number): number {
    return x + y;
};

调用签名

如果在对象类型中定义了调用签名类型成员,则称该对象类型为函数类型。语法示例如下

{
    (ParameterList): Type
}
  • ParameterList:函数形式参数列表
  • Type:函数返回值类型

使用示例如下

let f: { (x: number, y: number): number };
f = function (x: number, y: number): number {
    return x + y;
};

当对象类型字面量仅包含一个调用签名类型成员时,该对象类型可以简写为函数类型字面量。示例如下

{ (ParameterList): Type }
// 简写为
(ParameterList) => Type

示例如下

const f0: (x: number) => number = Math.abs;
const f1: { (x: nunber): number } = Math.abs;
f0(-1) === f1(-1); // true

构造函数类型字面量

语法示例如下

new (ParameterList) => Type
  • new:关键字
  • ParameterList:形式参数列表类型
  • Type:返回值类型

使用示例如下

let ErrorConstructor: new (message?: string) => Error;

构造签名

与调用签名类似。若在对象类型中定义了构造签名类型成员,则称该对象类型为构造函数类型。语法示例如下

{
    new (ParameterList): Type
}

使用示例如下

let Dog: { new (name: string): object };
Dog = class {
    private name: string;
    constructor(name: string) {
        this.name = name;
    }
};
let dog = new Dog('huahua');

与调用签名一样可以简写

{ new (ParameterList): Type }
// 简写为
new (ParameterList) => Type

调用签名与构造签名

如果在对象类型中同时定义调用签名和构造签名,则表示既可以被直接调用,又可以作为构造函数使用的函数类型。示例如下

declare const F: {
    new (x: number): Number;
    (x: number): number;
};
// 作为普通函数调用
const a: number = F(1);
// 作为构造函数调用
const b: Number = new F(1);

重载函数

重载函数是指一个函数同时拥有多个同类的函数签名。当使用不同数量和类型的参数调用重载函数时,可以执行不同的函数实现代码

TypeScript 中的重载函数与其他编程语言中的重载函数略有不同,且使用起来并不是很便利。因此实际上在大多数情况下并不需要声明重载函数,尤其是在函数返回值类型不变的情况下

重载函数的定义由以下两部分组成

  • 一条或多条函数重载语句
  • 一条函数实现语句

函数重载

不带有函数体的函数声明语句叫作函数重载。

  • 在函数重载中,不允许使用默认参数
  • 每个函数重载中的函数名和函数实现中的函数名必须一致
  • 各个函数重载语句之间以及函数重载语句与函数实现语句之间不允许出现任何其他语句

示例如下

function add(x: number, y: number): number;

函数实现

每个重载函数只允许有一个函数实现,并且它必须位于所有函数重载语句之后。示例如下

function add(x: number, y: number): number;
function add(x: any[], y: any[]): any[];
function add(x: number | any[], y: number | any[]): any {}

TypeScript 中的重载函数令人迷惑的地方在于:函数实现中的函数签名不属于重载函数的调用签名之一,只有函数重载中的函数签名能够作为重载函数的调用签名。

函数实现需要兼容每个函数重载中的函数签名,函数实现的函数签名类型必须能够赋值给函数重载的函数签名类型

在 TypeScript 中只允许存在一个函数实现,因此需要在这个函数实现中去实现所有函数重载的功能,这需要自行去检测参数的类型及数量,并根据判断结果去执行不同的操作。从这点来看,函数重载不是特别便利

函数重载解析顺序

一个函数重载需要满足如下条件才能成为本次函数调用的候选重载

  • 函数实际参数的数量不少于函数重载中定义的必选参数的数量
  • 函数实际参数的数量不多于函数重载中定义的参数的数量
  • 每个实际参数的类型能够赋值给函数重载定义中对应形式参数的类型

候选函数重载列表中的成员将以函数重载的声明顺序作为初始顺序,然后进行简单的排序,将参数类型中包含字面量类型的函数重载排名提前,然后使用排在第一的函数重载。示例如下

function f(x: string): void;
function f(y: 'specialized'): void;
function f(x: string) {}
f('specialized');

在使用字符串参数 specialized 调用时,两个函数重载都满足候选条件,因此都在函数重载列表中,但因为函数重载 2 的函数签名中包含字面量类型,所以优先级更高。因此最终构造出来的有序候选函数重载列表如下

  • 函数重载 2:function f(y: 'specialized'): void;
  • 函数重载 1:function f(x: string): void;

重载函数的类型

重载函数的类型可以通过包含多个调用签名的对象类型来表示。示例如下

function f(x: string): 0 | 1;
function f(x: any): number;
function f(x: any): any {}

可以使用如下对象类型字面量来表示重载函数 f 的类型

{
    (x: string): 0 | 1;
    (x: any): number;
}

在定义重载函数的类型时,有两点需要注意

  • 函数实现的函数签名不属于重载函数的调用签名之一
  • 调用签名的书写顺序是有意义的,它决定了函数重载的解析顺序,一定要确保更精确的调用签名位于更靠前的位置

对象类型字面量和接口都可以定义重载函数的类型

函数中 this 值的类型

默认情况下,编译器将函数中 this 值设置为 any 类型,并允许程序在 this 值上执行任意的操作

--noImplicitThis

启用该编译选项时,如果 this 值默认获得了 any 类型,则会产生编译错误

函数的 this 参数

TypeScript 支持在函数形式参数列表中定义一个特殊的 this 参数来描述该函数中 this 值的类型。示例如下

function f(this: { name: string }) {
    this.name = 'Patrick';
    this.name = 0; // 错误
}

this 参数固定使用 this 作为参数名

  • 是一个可选的参数,若存在,则必须作为函数形式参数列表中的第一个参数
  • 其参数的类型即为函数体中 this 值的类型
  • 只存在于编译阶段
  • 定义类型为 void 时,在无法在函数体内读写 this 的属性和方法
  • 调用定了 this 参数的函数时,若 this 值的实际类型与函数定义中的期望类型不匹配则会产生编译错误。示例如下
    function f(this: { bar: string }, baz: number) {}
    f(0); //错误
    f.call({ bar: 'hello' }, 0); // 正确
    

5.13 接口

  • 接口声明能够为对象类型命名
  • 接口类型无法表示原始类型

接口声明

语法示例如下

interface InterfaceName {
    TypeMember;
    TypeMember;
    ...
}
  • interface:关键字
  • InterfaceName:接口名,首字母需要大写
  • TypeMember:接口的类型成员

接口类型的类型成员也分为以下五类

  • 属性签名
  • 调用签名
  • 构造签名
  • 方法签名
  • 索引签名

这些类型成员的基本示例如下

interface CustomType {
    x: number; // 属性签名
    (message?: string): Error; // 调用签名
    new (message?: strnig): Error; // 构造签名
    get(arg: string): null; // 方法签名
    [prop: string]: number; // 字符串索引签名
    [prop: number]: string; // 数值索引签名
}

方法签名

语法示例如下

PropertyName(ParameterList): Type;
// 可改写为具有同等效果的属性签名
PropertyName: { (ParameterList): Type }
// 进一步改写
PropertyName: (ParameterList) => Type

若接口中包含多个名字相同但参数列表不同的方法签名成员,则表示该方法是重载方法。示例如下

interface A {
    f(): number;
    f(x: boolean): boolean;
    f(x: string, y: string): string;
}

索引签名

JavaScript 支持使用索引去访问对象属性。一个典型的例子是数组对象,既可以使用数字索引去访问数组元素,也可以使用字符串去访问数组对象上的属性和方法。示例如下

const colors = ['red', 'green', 'blue'];
const red = colors[0];
const len = colors['length'];

接口中的索引签名能够描述使用索引访问的对象属性的类型,索引签名只有以下两种

  • 字符串索引签名
  • 数值索引签名

字符串索引签名

语法示例如下

[IndexName: string]: Type
  • IndexName:索引名,可以为任意合法的标识符,只起到占位的作用,不代表真实的对象属性名
  • string:固定值,必须为 string 类型
  • Type:索引值的类型

一个接口中最多只能定义一个字符串索引签名,其会约束该对象类型中所有属性的类型。如字符串索引签名定义了索引值的类型为 number 类型,则该接口中所有属性的类型必须能够赋值给 number 类型。示例如下

interface A {
    [prop: string]: number;
    a: number;
    b: 0;
    c: 1 | 2;

    // 以下均会产生编译错误
    d: boolean;
    e: () => number;
    f(): number;
}

数值索引签名

语法示例如下

[IndexName: number]: Type
  • IndexName:索引名
  • number:固定值
  • Type:索引值的类型

一个接口中最多只能定义一个数值索引签名,其约束了数值属性名对应的属性值的类型。示例如下

interface A {
    [prop: number]: string;
}
const obj: A = ['a', 'b', 'c'];
obj[0]; // string

若接口中同时存在字符串索引签名和数值索引签名,则数值索引签名的类型必须能够赋值给字符串索引签名的类型,否则将产生编译错误。示例如下

interface A {
    [prop: string]: number;
    [prop: number]: 0 | 1;
}

可选属性与方法

示例如下

interface A {
    x?: string;
    y?(): number;
}

如果接口中定义了重载方法,则所有的重载方法签名必须同时为必选或者可选,否则将产生编译错误。示例如下

interface A {
    a(): void;
    a?(x: boolean): boolean; // 错误
}

只读属性与方法

示例如下

interface A {
    readonly a: string;
    readonly [prop: string]: string;
    readonly [prop: number]: string;
}

如果接口中既定义了只读索引签名,又定义了非只读的属性签名,则非只读的属性签名定义的属性依然是非只读的,除此之外的所有属性都是只读的。示例如下

interface A {
    readonly [prop: string]: number;
    x: number;
}
const a: A = { x: 0, y: 0 };
a.x = 1; // 正确
a.y = 1; // 错误

接口的继承

接口可以继承其他的对象类型,相当于将继承的对象类型中的类型成员复制到当前接口中。接口可以继承的对象类型如下

  • 接口

  • 对象类型的类型别名(参考 5.14 节)

  • 类(参考 5.15 节)

  • 对象类型的交叉类型(参考 6.4 节)

  • 接口的继承使用 extends 关键字

  • 同时继承多个接口时,父接口名之间使用逗号分隔

  • 一个接口继承了其他接口后,子接口既包含了自身定义的类型成员,也包含了父接口中的类型成员

  • 父子接口如果存在同名类型成员,子接口中的类型成员具有更高的优先级,且子类型中的同名类型成员的类型需要能够赋值给父接口中同名类型成员的类型,即必须是类型兼容的

  • 如果只是父接口之间存在同名的类型成员,而子接口本身没有该同名类型成员,则父接口中的同名类型成员的类型必须完全相同。解决办法为:在子接口中定义一个同名类型成员,注意要满足上一条约束

示例如下

interface Style {
    color: string;
    width: number;
}
interface Shape {
    name: string;
    width: string;
}
interface Circle extends Style, Shape {
    radius: number;
    color: number; // 错误,类型不兼容,父类型中的 color 为 string 类型
    width: number | string; // 子接口加上同名类型之后,就不会提示错误了
}
const c: Circle = {
    color: 'red',
    name: 'circle',
    radius: 1,
};

5.14 类型别名

类型别名声明能够为 TypeScript 中的任意类型命名。语法示例如下

type AliasName = Type;
  • type:关键字
  • AliasName:类型别名名称,须为合法的标识符
  • Type:类型别名关联的具体类型,可以为任意类型,或其他类型别名

类型别名不会创建一种新的类型,它只是给已有类型命名并直接引用该类型,通常用在一些比较复杂或书写起来比较长的类型

递归的类型别名

满足以下条件可以使用递归的类型别名

  • 若类型别名引用的类型为:接口类型、对象类型字面量、函数类型字面量、构造函数类型字面量。示例如下

    type T0 = { name: T0 };
    type T1 = () => T1;
    type T2 = new () => T2;
    
  • 若类型别名引用的数组类型或元组类型,则允许在元素类型中递归地引用类型别名。示例如下

    type T0 = Array<T0>;
    type T1 = T1[];
    type T2 = [number, T2];
    
  • 若类型别名引用的是泛型类或泛型接口,则允许在类型参数中递归地引用类型别名。示例如下

    interface A<T> {
        name: T;
    }
    type T0 = A<T0>;
    
    class B<T> {
        name: T | undefined;
    }
    tpye T1 = B<T1>;
    

使用递归的类型别名来定义 JSON 类型的例子。示例如下

type Json =
    | string
    | number
    | boolean
    | null
    | { [property: string]: Json }
    | Json[];
const data: Json = {
    name: 'TypeScript',
    version: { major: 3 },
};

类型别名与接口

两者区别如下

  • 类型别名能够表示非对象类型,接口只能表示对象类型
  • 接口可以继承其他的接口、类等对象类型,类型别名不支持继承(若类型别名表示对象时,可借助交叉类型来实现继承的效果)
  • 接口名总是会显示在编译器的诊断信息(若错误提示和警告)和代码编辑器的只能提示信息中,而类型别名的名字只在特定情况下才显示(当类型别名表示数组类型、元组类型、类或接口的泛型实例类型时,才会显示)
  • 接口具有声明合并的行为,类型别名不会进行合并

5.15 类

类的定义

类的定义有以下两种方式

  • 类声明
  • 类表达式

成员变量

在类中定义成员变量的方法。示例如下

class Circle {
    radius: number = 1;
}
// 或在构造函数中设置初始值
class Circle {
    radius: number;
    constructor() {
        this.radius = 1;
    }
}

--strictPropertyInitialization

该编译选项必须和 --strictNullChecks 同时启用。启用时,成员变量必须在声明时进行初始化,或者在构造函数中进行初始化。在构造函数进行初始化的时候,须直接初始化,若想间接初始化,则需要使用非空断言。示例如下

class A {
    a: number; // 错误,未初始化
    b: number; // 正确
    c!: number; // 正确
    constructor() {
        this.b = 1;
        this.init();
    }
    init() {
        this.a = 0;
        this.c = 1;
    }
}

readonly 属性

须在声明时初始化或者在构造函数例初始化。示例如下

class A {
    readonly a = 0;
    readonly b: number;
    readonly c: number; // 错误
    constructor() {
        this.b = 0;
    }
}

成员函数

成员函数也称为方法。示例如下

class Circle {
    radius: number = 1;
    area(): number {
        return Math.PI * this.radius * this.radius;
    }
}

成员存取器

成员存取器由 getset 方法构成,并且会在类中声明一个属性。成员存取器的定义与对象字面量中属性存取器的定义方式完全相同

  • 如果同时定义了 getset,则 get 方法的返回值类型必须与 set 方法的参数类型一致
  • 如果同时定义了 getset,则 getset 必须具有相同的可访问性

索引成员

类的索引成员会在类的类型中引入索引签名。索引签名包含两种

  • 字符串索引签名
  • 数值索引签名

实际应用中,定义类的索引成员不常见

  • 类中所有的属性和方法必须符合字符串索引签名定义的类型,同时只有当类具有类似数组的行为时,数值索引签名才有意义
  • 类的索引成员与接口中的索引签名类型成员具有完全相同的语法和语义,具体参考 5.13.6 节
  • 类的索引成员上不允许定义可访问性修饰符

成员可访问性

TypeScript 为类成员提供了三种可访问性修饰符

  • public:默认情况下,类的所有成员都是公有成员,可以省略 public 修饰符,类的公有成员没有访问限制,可以在当前类的内部、外部、派生类的内部访问
  • protected:类的受保护成员允许在当前类的内部和派生类的的内部访问,不允许在当前类的外部访问
  • private:类的私有成员只允许在当前类的内部被访问
  • 私有字段:在 ECMAScript 标准中,类的私有字段使用 # 符号来表示,无论是在定义还是访问时,都需要带上该符号。示例如下
    class Circle {
        #radius: number;
        constructor() {
            this.#radius = 1;
        }
    }
    const circle = new Circle();
    circle.#radius; // 不允许访问
    

构造函数

构造函数用于创建和初始化类的实例

  • constructor 作为函数名
  • 可以定义可选参数、默认值参数、剩余参数
  • 不允许定义返回值类型,因为构造函数的返回值类型永远为类的实例类型
  • 可以使用可访问性修饰符,如果将构造函数设置为私有的,则只允许在类的内部创建该类的对象。示例如下
    class Singleton {
        private static instance?: Singleton;
        private constructor() {}
        static getInstance() {
            if (!Singleton.instance) {
                // 允许访问
                Singleton.instance = new Singleton();
            }
            return Singleton.instance;
        }
    }
    new Singleton(); // 错误
    
  • 支持重载。示例如下
    class A {
        constructor(x: number, y: number);
        constructor(s: stirng);
        constructor(xs: number | string, y?: number) {}
    }
    const a = new A(0, 0);
    const b = new A('foo');
    

参数成员

TypeScript 提供了一种简洁语法能够把构造函数的形式参数声明为类的成员变量,称为参数成员。在构造函数参数列表中,为形式参数添加任何一个可访问性修饰符或者 readonly 修饰符,该形式参数就成了参数成员,进而会被声明为类的成员变量。示例如下

class A {
    constructor(public x: number, public readonly y: number) {}
}
const a = new A(0);
a.x; // 0

继承

语法示例如下

class DerivedClass extends BaseClass {}
  • BaseClass:基类,也叫父类
  • DerivedClass:派生类,也叫子类

当派生类继承了基类后,就自动继承了基类的非私有成员

重写基类成员

  • 在派生类中定义与基类中同名的成员变量和成员函数,则可以重写基类对应的成员
  • 若派生类重写了基类中的受保护成员,则可以将该成员的可访问性设置为受保护的或公有的(即只允许放宽可访问性)
  • 重写基类的成员时,需要保证子类型兼容性

派生类实例化

在派生类的构造函数中必须调用基类的构造函数,使用 super() 语句即可

在实例化派生类时的初始化顺序如下

  • 初始化基类属性
  • 调用基类的构造函数
  • 初始化派生类的属性
  • 调用派生类的构造函数

单继承

TypeScript 中的类仅支持单继承。示例如下

class C extends A, B {} // 错误,类只能继承一个类

接口继承类

若接口继承了一个类,则该接口会继承基类中所有成员的类型,包括受保护成员类型和私有成员类型。示例如下

class A {
    x: string = '';
    y(): boolean {
        return true;
    }
}
interface B extends A {}
declare const b: B;
b.x; // 类型为 string
b.y(); // 类型为 boolean

如果接口从基类继承了非公有成员,则该接口只能由基类或基类的子类来实现。示例如下

class A {
    private x: string = '';
    protected y: string = '';
}
// 接口 I 能够继承 A 的私有属性和受保护属性
interface I extend A {}
// 正确,B 可以实现接口 I,因为私有属性和受保护属性源自同一个类 A
class B extends A implements I {}
// 错误,C 不是 A 的子类,无法实现 A 的私有属性和受保护属性
class C implements I {}

实现接口

类可以实现一个或多个接口,使用 implements 语句能够声明类所实现的接口。示例如下

interface A {}
interface B {}
class C implements A, B {}

如果类的定义中声明了要实现的接口,则这个类就需要实现接口中定义的类型成员。示例如下

interface Color {
    color: string;
}
interface Shape {
    area(): number;
}
class Circle implements Shape, Color {
    radius: number = 1;
    color: string = 'black';
    area(): number {
        return Math.PI * this.radius * this.radius;
    }
}

静态成员

类的静态成员不属于类的某个实例,而是属于类本身。类的静态成员使用 static 关键字定义,并且只允许通过类名访问

静态成员可访问性

  • 类的 public 静态成员对访问没有限制
  • 类的 protected 静态成员允许在当前类的内部和派生类的内部访问,但是不允许在当前类的外部访问
  • 类的 private 静态成员只允许在当前类的内部访问

继承静态成员

类的 public 静态成员和 protected 静态成员可以被继承

抽象类和抽象成员

使用 abstract 关键字定义抽象类和抽象类成员

抽象类

  • 抽象类不能被实例化
  • 其主要作用是作为基类使用
  • 抽象类可以继承其他抽象类
  • 抽象类中允许包含抽象成员,也允许包含非抽象成员
abstract class A {}
const a = new A(); // 错误

抽象成员

  • 抽象成员不允许包含具体实现代码
  • 如果一个具体类继承了抽象类,则在具体的派生类中必须实现抽象类基类中的所有抽象成员
  • 抽象类中的抽象成员不能声明为 private,否则将无法在派生类中实现该成员
abstract class Base {
    abstract a: string;
    abstract method(): boolean;
}
class Derived extends Base {
    a: string = '';
    methods(): boolean {
        return true;
    }
}

this 类型

在类中存在一种特殊的 this 类型,它表示当前 this 值的类型。可以在类的非静态成员的类型注解中使用 this 类型。示例如下

class Counter {
    private count: number = 0;
    public add(): this {
        this.count++;
        return this;
    }
    public subtract(): this {
        this.count--;
        return this;
    }
    public getResult(): number {
        return this.count;
    }
}
const counter = new Counter();
counter.add().add().subtract().getResult(); // 1

this 类型是动态的,表示当前 this 值的类型。当前 this 值的类型不一定是引用了 this 类型的那个类,该差别主要体现在类之间有继承关系的时候。示例如下

class A {
    foo(): this {
        return this;
    }
}
class B extends A {
    bar(): this {
        return this;
    }
}
const b = new B();
const x = b.bar().foo(); // x 的类型为 B

类类型

类声明将会引入一个新的命名类型,即与类同名的类类型。类类型表示类的实例类型,它由类的实例成员构成。示例如下

class Circle {
    radius: number;
    area(): number {
        return Math.PI * this.radius * this.radius;
    }
}
interface CircleType {
    radius: number;
    area(): number;
}
const a: Circle = new Circle();
const b: CircleType = new Circle();

在定义一个类时,实际上是定义了一个构造函数,随后可以使用 new 运算符和该构造函数来创建类的实例。可以将该类型称为类的构造函数类型,在该类型中也包含了类的静态成员类型。示例如下

class A {
    static x: number = 0;
    y: number = 0;
}
// 类类型,即实例类型
const a: A = new A();
interface AConstructor {
    new (): A;
    x: number;
}
// 类构造函数类型
const b: AConstructor = A;
Last Updated:
Contributors: zhangfei