Skip to content

TypeScript

any 类型,unknown 类型,never 类型

any

any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。 从集合论的角度看,any 类型可以看成是所有其他类型的全集,包含了一切可能的类型。TypeScript 将这种类型称为“顶层类型”(top type),意为涵盖了所有下层。

类型推断问题

如果无法推断出类型,TypeScript 就会认为该变量的类型是 any

unknown

为了解决 any 类型“污染”其他变量的问题,TypeScript 3.0 引入了 unknown 类型。

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

2.不能直接调用 unknown 类型变量的方法和属性

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

never

由于不存在任何属于“空类型”的值,所以该类型被称为 never,即不可能有这样的值。 为什么 never 类型可以赋值给任意其他类型呢?这也跟集合论有关,空集是任何集合的子集。TypeScript 就相应规定,任何类型都包含了 never 类型。因此,never 类型是任何其他类型所共有的,TypeScript 把这种情况称为“底层类型”(bottom type)。

TIP

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

类型系统

基本类型

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

boolean string number bigint symbol object undefined null

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

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

这 8 种基本类型是 TypeScript 类型系统的基础,复杂类型由它们组合而成。

包装对象类型

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

boolean string number bigint symbol 五种包装对象之中,symbol 类型和 bigint 类型无法直接获取它们的包装对象(即 Symbol()和 BigInt()不能作为构造函数使用),但是剩下三种可以。

Boolean() String() Number() 注意,String()只有当作构造函数使用时(即带有 new 命令调用),才会返回包装对象。如果当作普通函数使用(不带有 new 命令),返回就是一个普通字符串。其他两个构造函数 Number()和 Boolean()也是如此。

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

由于包装对象的存在,导致每一个原始类型的值都有包装对象和字面量两种情况。 Boolean 和 boolean String 和 string Number 和 number BigInt 和 bigint Symbol 和 symbol 其中,大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量,不包含包装对象。

Object 类型与 object 类型

大写的 Object 类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值,都是 Object 类型,这囊括了几乎所有的值。 除了 undefined 和 null 这两个值不能转为对象,其他任何值都可以赋值给 Object 类型。 另外,空对象{}是 Object 类型的简写形式,所以使用 Object 时常常用空对象代替。

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

注意,无论是大写的 Object 类型,还是小写的 object 类型,都只包含 JavaScript 内置对象原生的属性和方法,用户自定义的属性和方法都不存在于这两个类型之中。

undefined 和 null 的特殊性

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

作为值,它们有一个特殊的地方:任何其他类型的变量都可以赋值为 undefined 或 null。 这并不是因为 undefined 和 null 包含在 number 类型里面,而是故意这样设计,任何类型的变量都可以赋值为 undefined 和 null,以便跟 JavaScript 的行为保持一致。

TypeScript 提供了一个编译选项 strictNullChecks。只要打开这个选项,undefined 和 null 就不能赋值给其他类型的变量(除了 any 类型和 unknown 类型)。 总之,打开 strictNullChecks 以后,undefined 和 null 只能赋值给自身,或者 any 类型和 unknown 类型的变量。

值类型

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

TypeScript 推断类型时,遇到 const 命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。 这样推断是合理的,因为 const 命令声明的变量,一旦声明就不能改变,相当于常量。值类型就意味着不能赋为其他值。

联合类型

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

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

交叉类型

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

交叉类型 A&B 表示,任何一个类型必须同时属于 A 和 B,才属于交叉类型 A&B,即交叉类型同时满足 A 和 B 的特征。 交叉类型的主要用途是表示对象的合成。 交叉类型常常用来为对象类型添加新属性。

type 命令

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

别名不允许重名。 别名的作用域是块级作用域。这意味着,代码块内部定义的别名,影响不到外部。

typeof 运算符

JavaScript 语言中,typeof 运算符是一个一元运算符,返回一个字符串,代表操作数的类型。 JavaScript 里面,typeof 运算符只可能返回八种结果,而且都是字符串。

typeof undefined; // "undefined"

typeof true; // "boolean"

typeof 1337; // "number"

typeof "foo"; // "string"

typeof {}; // "object"

typeof parseInt; // "function"

typeof Symbol(); // "symbol"

typeof 127n // "bigint"

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

块级类型声明

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

类型的兼容

TypeScript 为这种情况定义了一个专门术语。 如果类型 A 的值可以赋值给类型 B,那么类型 A 就称为类型 B 的子类型(subtype)。 TypeScript 的一个规则是,凡是可以使用父类型的地方,都可以使用子类型,但是反过来不行。 之所以有这样的规则,是因为子类型继承了父类型的所有特征,所以可以用在父类型的场合。但是,子类型还可能有一些父类型没有的特征,所以父类型不能用在子类型的场合。

数组类型

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

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

ts
// 数组的类型有两种写法。第一种写法是在数组成员的类型后面,加上一对方括号。
let arr: number[] = [1, 2, 3];
// 数组arr的成员类型是number|string
let arr: (number | string)[];

// 数组类型的第二种写法是使用 TypeScript 内置的 Array 接口。
let arr: Array<number> = [1, 2, 3];
let arr: Array<number | string>;

TypeScript 允许使用方括号读取数组成员的类型。

ts
type Names = string[];
type Name = Names[0]; // string
type Name = Names[number]; // string

数组的类型推断

如果变量的初始值是空数组,那么 TypeScript 会推断数组类型是 any[]。

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

只读数组,const 断言

TypeScript 允许声明只读数组,方法是在数组类型前面加上 readonly 关键字。

注意,readonly 关键字不能与数组的泛型写法一起使用。 实际上,TypeScript 提供了两个专门的泛型,用来生成只读数组的类型。

ts
const a1: ReadonlyArray<number> = [0, 1];

const a2: Readonly<number[]> = [0, 1];

只读数组还有一种声明方法,就是使用“const 断言”。 const arr = [0, 1] as const;

多维数组

TypeScript 使用 T[][]的形式,表示二维数组,T 是最底层数组成员的类型。 var multi:number[][] = [[1,2,3], [23,24,25]]; 上面示例中,变量 multi 的类型是 number[][],表示它是一个二维数组,最底层的数组成员类型是 number。

元组类型

const s:[string, string, boolean] = ['a', 'b', true];

使用元组时,必须明确给出类型声明(上例的[number]),不能省略,否则 TypeScript 会把一个值自动推断为数组。 元组成员的类型可以添加问号后缀(?),表示该成员是可选的。 注意,问号只能用于元组的尾部成员,也就是说,所有可选成员必须在必选成员之后。

symbol 类型

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

类型推断

let 命令声明的变量,推断类型为 symbol。 const 命令声明的变量,推断类型为 unique symbol。

函数类型

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

如果参数不指定参数类型,TypeScript 就会推断参数类型,如果缺乏足够信息,就会推断该参数的类型为 any。

返回值的类型通常可以不写,因为 TypeScript 自己会推断出来。

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

函数的实际参数个数,可以少于类型指定的参数个数,但是不能多于,即 TypeScript 允许省略参数。

ts
// 函数类型还可以采用对象的写法。
{
  (参数列表): 返回值
}
let add:{
  (x:number, y:number):number
};
add = function (x, y) {
  return x + y;
};

// 函数类型也可以使用 Interface 来声明,这种写法就是对象写法的翻版
interface myfn {
  (a:number, b:number): number;
}
var add:myfn = (a, b) => a + b;

Function 类型

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

箭头函数

可选参数

参数名带有问号,表示该参数的类型实际上是原始类型|undefined。 函数的可选参数只能在参数列表的尾部,跟在必选参数的后面

参数默认值

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

设置了默认值的参数,就是可选的。如果不传入该参数,它就会等于默认值。 具有默认值的参数如果不位于参数列表的末尾,调用时不能省略,如果要触发默认值,必须显式传入 undefined。

参数解构

ts
// 函数参数如果存在变量解构,类型写法如下。
function f([x, y]: [number, number]) {
  // ...
}

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

// 参数解构可以结合类型别名(type 命令)一起使用,代码会看起来简洁一些。
type ABC = { a: number; b: number; c: number };

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

rest 参数

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

readonly 只读参数

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

注意,readonly 关键字目前只允许用在数组和元组类型的参数前面,如果用在其他类型的参数前面,就会报错。

void 类型

void 类型表示函数没有返回值。 void 类型允许返回 undefined 或 null。 如果打开了 strictNullChecks 编译选项,那么 void 类型只允许返回 undefined。 如果返回 null,就会报错。这是因为 JavaScript 规定,如果函数没有返回值,就等同于返回 undefined。

never 类型

never 类型表示肯定不会出现的值。它用在函数的返回值,就表示某个函数肯定不会返回值,即函数不会正常执行结束。 它主要有以下两种情况。 (1)抛出错误的函数。 (2)无限执行的函数。

注意,never 类型不同于 void 类型。前者表示函数没有执行结束,不可能有返回值;后者表示函数正常执行结束,但是不返回值,或者说返回 undefined。 如果一个函数抛出了异常或者陷入了死循环,那么该函数无法正常返回一个值,因此该函数的返回值类型就是 never。如果程序中调用了一个返回值类型为 never 的函数,那么就意味着程序会在该函数的调用位置终止,永远不会继续执行后续的代码。 一个函数如果某些条件下有正常返回值,另一些条件下抛出错误,这时它的返回值类型可以省略 never。

高阶函数

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

函数重载

有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)。 TypeScript 对于“函数重载”的类型声明方法是,逐一定义每一种情况的类型。

重载声明的排序很重要,因为 TypeScript 是按照顺序进行检查的,一旦发现符合某个类型声明,就不再往下检查了,所以类型最宽的声明应该放在最后面,防止覆盖其他类型声明。 由于重载是一种比较复杂的类型声明方法,为了降低复杂性,一般来说,如果可以的话,应该优先使用联合类型替代函数重载,除非多个参数之间、或者某个参数与返回值之间,存在对应关系。

构造函数

JavaScript 语言使用构造函数,生成对象的实例。 构造函数的最大特点,就是必须使用 new 命令调用。 构造函数的类型写法,就是在参数列表前面加上 new 命令。

ts
class Animal {
  numLegs: number = 4;
}

type AnimalConstructor = new () => Animal;

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

// 构造函数还有另一种类型写法,就是采用对象形式。
type F = {
  new (s: string): object;
};

某些函数既是构造函数,又可以当作普通函数使用,比如 Date()。这时,类型声明可以写成下面这样。

ts
type F = {
  new (s: string): object;
  (n?: number): number;
};

上面示例中,F 既可以当作普通函数执行,也可以当作构造函数使用。

对象类型

对象类型的最简单声明方法,就是使用大括号表示对象,在大括号内部声明每个属性和方法的类型。 属性的类型可以用分号结尾,也可以用逗号结尾。 一旦声明了类型,对象赋值时,就不能缺少指定的属性,也不能有多余的属性。 除了 type 命令可以为对象类型声明一个别名,TypeScript 还提供了 interface 命令,可以把对象类型提炼为一个接口。

可选属性

只读属性

ts
interface MyInterface {
  readonly prop: number;
}

属性名的索引类型

ts
type MyObj = {
  [property: string]: string;
};

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

解构赋值

解构赋值用于直接从对象中提取属性。 注意,目前没法为解构变量指定类型,因为对象解构里面的冒号,JavaScript 指定了其他用途。

ts
let { x: foo, y: bar }: { x: string; y: number } = obj;

结构类型原则

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

严格字面量检查

如果对象使用字面量表示,会触发 TypeScript 的严格字面量检查(strict object literal checking)。如果字面量的结构跟类型定义的不一样(比如多出了未定义的属性),就会报错。 如果等号右边不是字面量,而是一个变量,根据结构类型原则,是不会报错的。 TypeScript 对字面量进行严格检查的目的,主要是防止拼写错误。一般来说,字面量大多数来自手写,容易出现拼写错误,或者误用 API。 规避严格字面量检查,可以使用中间变量。 如果你确认字面量没有错误,也可以使用类型断言规避严格字面量检查。 编译器选项 suppressExcessPropertyErrors,可以关闭多余属性检查。

最小可选属性规则

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

为了避免这种情况,TypeScript 2.4 引入了一个“最小可选属性规则”,也称为“弱类型检测”(weak type detection)。 如果某个类型的所有属性都是可选的,那么该类型的对象必须至少存在一个可选属性,不能所有可选属性都不存在。这就叫做“最小可选属性规则”。

空对象

空对象是 TypeScript 的一种特殊值,也是一种特殊类型。 空对象没有自定义属性,所以对自定义属性赋值就会报错。空对象只能使用继承的属性,即继承自原型对象 Object.prototype 的属性。 TypeScript 不允许动态添加属性,所以对象不能分步生成,必须生成时一次性声明所有属性。

interface 接口

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

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

如果一个 interface 同时定义了字符串索引和数值索引,那么数值索引必须服从于字符串索引。因为在 JavaScript 中,数值属性名最终是自动转换成字符串属性名。

对象的方法共有三种写法。

ts
// 写法一
interface A {
  f(x: boolean): string;
}

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

// 写法三
interface C {
  f: { (x: boolean): string };
}

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

ts
interface Add {
  (x: number, y: number): number;
}

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

ts
interface ErrorConstructor {
  new (message?: string): Error;
}

interface 的继承

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

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

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