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,即继承该类的所有成员。