Skip to content

TypeScript 基础类型

原始类型 ​

TypeScript 中的原始类型和 JavaScript 中的原始类型非常相似,包括 number,string,boolean,null,undefined,symbol,bigint。但同时还有两个特殊的类型

  1. void 表示一个值不存在,只用于函数的返回值
  2. nullable 表示一个值可能为 null 或者 undefined

枚举类型 ​

enum 枚举类型分为三类,数字型,字符型,异构型,他们都会被编译成对象,使用方法如下

typescript
enum Foo {
  A = 1,
  B = 2,
  C = "3",
}
const num: Foo = Foo.A;

其中 Foo 被称作联合枚举类型,他的字段 Foo.A 被称作联合枚举成员类型

数字型 ​

前面提到枚举类型都会被编译成一个对象,作为一个子集,数字型枚举类型自然也会被编译成一个对象,但他同时还有一个特殊的行为:反向映射

typescript
enum Direction {
  Up, // 如果没有赋值,默认为 0
  Down, // 后续的值会递增,因此这个值为 1
  Left, // 2
  Right, // 3
}

会被编译成

javascript
var Direction = {
  0: "Up",
  1: "Down",
  2: "Left",
  3: "Right",
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 3,
};

从而可以实现根据值获取名字的功能,但是这个功能只能在数字型枚举类型上使用,因为反向映射的行为存在一定的运行时开销,因此字符型和异构型枚举类型不会被编译成这种形式。

字符型 ​

字符型必须要有初始化的过程,初始化的值必须是字符串字面量或者另一个字符型枚举类型的成员:

typescript
enum StringDirection {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

会被编译成

javascript
var StringDirection = {
  Up: "UP",
  Down: "DOWN",
  Left: "LEFT",
  Right: "RIGHT",
};

异构型 ​

不推荐在代码中使用

TIP

这里补充一下单例类型(单元类型)这个概念,因为仅包含一个可能的值,因此被称作单例类型,比如 undefined,void,null,字面量 等,实际上这个是另一个维度的分类,因此可能和上面有重叠。

const 枚举类型 ​

当我们不需要 enum 的一些骚操作的时候

顶端类型 ​

顶端类型是所有类型的父类型,在 TypeScript 中包含两种顶端类型

any ​

因为是所有类型的父类型,因此可以将任意类型的变量赋值给一个 any 类型的变量,同时 any 类型变量也可以赋值给任意类型的其他变量(包含他自身),因此可以合理的利用这个类型来跳过类型检查。

同时,如果一个变量无法推导出类型,默认也会变成 any 类型。为了避免滥用,可以适当调整编译选项,具体见 TypeScript 编译配置

unknown ​

这是更安全的一种顶端类型,我们可以将任意类型赋值给 unknown 类型变量(与 any 相同),但是只能将 unknown 赋值给 any 和他自身。

在执行一些算术操作时候,也必须要先细化类型,否则会爆出编译错误。

尾端类型 ​

尾端类型是其他所有类型的子类型,因此尾端类型不存在一个值来表示,只有它自身可以给它赋值,即使是 any 类型也无法赋值给它。TypeScript 中仅有一个尾端类型 never。

never

尾端类型主要在函数返回值上发光发热,他表示函数不可能返回值,这和 void 有区别,前者意思为函数不可能结束(由于异常,或者死循环),后者表示函数可以执行完,但是没有返回值。

还有一个地方也会用到 never,就是类型体操……有一些工具类型会用到它,比如 Exclude<T, U> = T extends U ? never : T

数组类型 ​

主要是有两种声明方式,简单类型的数组可以直接通过方括号来定义,比较复杂的数组类型倾向于使用泛型来定义。

这里还涉及只读类型数组的定义,有三种方法:

  1. 使用 ReadonlyArray<T> 来定义

  2. 使用 readonly 修饰符

  3. 使用 Readonly<T> 工具类型来定义,这个工具类型定义如下:

    typescript
    type Readonly<T> = {
      readonly [K in keyof T]: T[K];
    };

    他只可以用用在对象身上,因此定义一个只读数组的代码如下 Readonly<number[]>

元组类型 ​

元组是由有限个元素组成的有序列表,一个简单定义(经典元组)如下:

typescript
const point: [number, number] = [0, 0];

我们还可以使用问号来表示可选元素(和 JavaScript 一样),或者使用剩余元素语法:

typescript
const point: [number, number, number?]; // 使用可选元素语法
const tuple: [number, ...string[]]; // 使用剩余元素语法

注意拓展操作符必须在元组类型中才能使用,常用于类型体操的 concat 操作,目前看来只有元组有这个拓展语法,对象似乎没有(不像 JavaScript 一样)

在获取一个元组类型的对象的长度时,TypeScript 编译器会尽可能的利用元组的定义信息来推断长度,比如上面的 point 的长度的类型就是一个字面量的联合类型 2|3。

同时注意,元组类型是数组类型的子类型,因此可以赋值给数组类型,反之不可以

对象类型 ​

TypeScript 中提供了多种定义对象的方法,我们先介绍三种对象类型:

  1. Object 类型
  2. object 类型
  3. 对象字面量

Object 类型 ​

注意这里的 Object 是大写的,但这并不意味着这是一个构造函数的类型,我们首先将这两个区分一下

typescript
const o = new Object();
const p: Object;

首先 o 变量他是 Object 的类型,而构造函数 Object 的类型是 ObjectConstructor,这个构造函数变量的定义如下:

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

可以看到 Object 构造函数的原型对象 prototype 是 Object 类型

从这里我们简单引出类型 Object 的定义,它实际上是 Object 构造函数的原型对象的类型,尽量不要用在其他的变量上。他的类型定义为

typescript
interface Object {
  constructor: Function;
  toString(): string;
  valueOf(): Object;
  // ...
}

Object 类型兼容性很好(或者说太过宽泛了……),除了 null 和 undefined 都可以赋值给他,基本类型也可以,这是因为基本类型在赋值的时候可能有一个装包(封装为对应类型的对象)的过程。

所以我们在描述一个对象的类型的时候,尽量不要使用 Object,他只适合用来描述 Object.prototype 这一个变量

object 类型 ​

注意这里开头为小写,它强调变量类型为非原始类型,但它并不关心变量内部的字段有什么。

在类型兼容方面,任何对象和顶端类型都可以赋值给他,因此范围在 Object 的基础上缩小了。

对象字面量 ​

首先说一下对象字面量的语法,我们先写出一个大括号,然后内部再依次写下属性的类型成员,类型成员包括 key 和 type,共有五种:

  1. 属性签名 PropertyName: Type,其中 PropertyName 也可以是一个计算属性,只需要保证类型是 string | number | Symbol 就可以

    这些属性还可以设置是否只读,是否可选这两种选项。如果一个对象至少有一个属性,并且所有的属性都是可选的,那么我们称呼他为 弱类型,也许这个弱类型很适合作为用户输入来表示。

  2. 调用签名 {(paramList): Type} 因为函数实质上也是一个对象(唯一区别是这个对象可以被调用),因此可以写一个调用签名来表示这个对象可以调用。而函数类型字面量实质上也与仅包含一个调用签名的对象字面量等价,前者是后者的简写罢了

    typescript
    {(ParamList): Type}
    // 等价于
    (ParamList) => Type

    前者的表现力更强,可以描述一个函数的更多信息,比如 vue 中就有用到这种类型声明。

  3. 构造签名 new (paramList): Type 其实同上,他也是构造函数字面量的复杂写法。

  4. 方法签名 PropertyName(ParamList): Type 这个和调用签名是不一样的,调用签名是描述对象本身可以被调用的形式,而方法签名描述的函数类型的属性成员。

  5. 索引签名 [indexName: string]: Type 这里面的 indexName 是索引名,但实际上并没有什么作用,真正起作用的是后面标注的索引类型 string 表示这个对象的索引必须是字符串类型,同理,除了字符串类型,还有数字类型可以选择

    但注意,JavaScript 中表面看有整数索引,但实际上是将整数转换成字符串

函数类型 ​

我们这里不介绍基础的内容,先讲一下函数重载,先看一个实例:

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

要注意前两行代码是函数重载,最后一行是函数定义,函数定义的类型需要兼容所有函数重载。

而且函数重载必须紧挨着,不能有空行之类的(会编译错误),他们的顺序也是有要求的,必须精确的重载在前,模糊的在后

所以现在看来 TypeScript 的函数重载并不是很实用。还不如如下的定义:

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

我们仅定义一个对象,给他两个调用签名。


除了函数重载,还可以定义 this 的类型,例子如下:

typescript
function add(this: void, x: number, y: number) {
  this.name = "sth"; // error
}

我们可以这里函数定义多了一个 this,这不意味着我们在调用时需要传入一个 this 参数,编译器会自动清除这个参数,他只是提供类型检查信息的。如果使用 TypeScript 写小程序,你也许经常会用到这种写法

接口类型 ​

接口类型和对象字面量类型很相似,都只存在于编译阶段,编译生成后的 TypeScript 不存在相关代码。他和对象字面量类型区别只是在对象字面量前面加上了 interface 关键字和一个接口名(接口名首字母大写),其内部属性的定义也是一样的,包含那五种类型成员。因此我们这里只介绍接口特有的功能。

继承 ​

接口可以继承以下类型

  • 接口
  • 对象类型的类型别名(下一小节会提到)
  • 类
  • 对象类型的交叉类型(下一篇文章有提到)

继承会使用 extends 关键字,并且一个接口可以继承多个目标。但这个继承的过程可能发生字段的冲突,我们思考一下

  1. 父子接口的字段冲突:

    如果发生冲突,子接口的相同字段有更高的优先级,但需要保证子接口的类型能够赋值给父接口的同名字段,比如

    typescript
    interface A {
      o: { a: number };
    }
    interface B extends A {
      o: { a: number; b: number };
    }
  2. 多个父接口之间的冲突

    如果发生冲突,父接口之间的成员类型必须是完全相同的,否则会无法编译(报错)

    但不是没有解决方案,我们可以在子接口上定义同名字段,这个同名字段同时可以赋值给所有包含该字段的父接口,相当于做了一个垫片。

    typescript
    interface A {
      o: { a: number };
    }
    interface B {
      o: { hello: string };
    }
    interface C extends A, B {
      o: { a: number; hello: string };
    }

类型别名 ​

类型别名和接口很相似,接口是给对象字面量起个名字,类型别名是给任意类型起个名字。类型别名的首字母同样需要大写。我们只谈论类型别名特有的功能

递归类型别名 ​

三种情况下可以递归的使用类型别名

  1. 别名引用的是对象字面量,接口或者函数字面量
  2. 别名引用的是一个数组
  3. 别名引用的是一个泛型类或者接口

我们以 JSON 类型为例,看一下是如何使用的:

typescript
type Json =
  | string
  | number
  | boolean
  | null
  | { [property: string]: Json }
  | Json[];

这里可以看到涉及了第一种和第二种情况

类型别名和接口的区别 ​

  1. 类型别名可以用在非对象类型上
  2. 接口可以实现继承,类型别名只能实现类似继承的效果
  3. 接口名提供的类型诊断信息更清晰,类型别名会显示实际的内容,可能会很杂乱
  4. 接口有声明合并的效果,如果重复声明了两个相同的接口,他们的类型最终会合并,至于冲突的字段,我们后面会提到

类 ​

类在 JavaScript 中本身就是函数的一个语法糖,TypeScript 对其进一步加强,我们只讲类的一些特殊的语法

在创建一个类的实例时,我们会使用到构造函数进行类成员的初始化,TypeScript 可以直接在定义类成员时初始化,如下:

typescript
class Foo {
  a = 1;
  b = 2;

  constructor() {
    this.a = 3;
  }
}

这个语法是可选的,但是对每个成员变量都进行初始化是一个好习惯,因为这样可以避免一些意外的错误。为此可以开启 --strictPropertyInitialization 选项,TypeScript 编译器会自动为你检查是否所有的成员变量都被初始化。遗憾的是,编译器并不能覆盖所有情况,如果你在构造函数中调用其他函数间接初始化变量,TypeScript 编译器无法识别这个情况,会给你爆出恼人的错误,这时候可以通过非空断言 ! 来解决错误

类可以定义访问控制等级,比如 private,protected,public,还可以使用 static 定义是否是静态成员

有趣的是,我们可以对构造函数进行访问控制,比如给他 private 访问控制,这样就无法实例化这个类,但是可以通过静态方法来实现单例模式。

类也可以实现继承,其语法和接口继承一样,通过 extends 实现,但是只允许单继承一个类,继承类的时候需要用 super 关键字调用父类的构造函数。类还可以实现接口,对于实现接口的数量没有要求。

类本身还可以定义是否是抽象类,具体使用到的时候再讲。