Skip to content

TypeScript 的类型系统

基本类型

概述

JavaScript 语言将值分成 8 种类型。

  • boolean

  • string

  • number

  • undefined

  • null

  • symbol

  • bigint

  • object (array, function)

TypeScript 继承了这一点,以上 8 种类型可以看作 TypeScript 的基本类型

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

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

复杂类型由它们组合而成。

boolean 类型

boolean类型只包含truefalse两个布尔值。

string 类型

string类型包含所有字符串。

number 类型

number类型包含所有的数字,整数、浮点数和非十进制数都属于 number 类型。

const x: number = 123;
const y: number = 3.14;
const z: number = 0xffff;

bigint 类型

bigint 类型包含所有的大整数。

const x: bigint = 123n;
const y: bigint = 0xffffn;

上面示例中,变量xy就属于 bigint 类型。

bigint 与 number 类型不兼容。

注意,bigint 类型是 ES2020 引入的。如果使用这个类型,TypeScript 编译的 target 不能低于 es2020

symbol 类型

symbol 类型包含所有的 Symbol 值。

object 类型

根据 JavaScript 的设计,对象、数组、函数都属于 object 类型。

undefined 类型,null 类型

undefinednull既是值,又是类型。

注意,如果没有声明类型的变量,被赋值为undefinednull,它们的类型会被推断为any

ts
let a = undefined; // any
const b = undefined; // any

let c = null; // any
const d = null; // any

TS 提供了一个编译选项strictNullChecks。只要打开这个选项,undefinednull就不能赋值给其他类型的变量(除了any类型和unknown类型),否则,它们可以被赋值给任意类型而不报错

ts
// 打开编译设置 strictNullChecks
let a = undefined; // undefined
const b = undefined; // undefined

let c = null; // null
const d = null; // null

这个选项在配置文件tsconfig.json的写法如下。

json
{
  "compilerOptions": {
    "strictNullChecks": true
    // ...
  }
}

包装对象类型

包装对象

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

  • boolean
  • string
  • number
  • bigint
  • symbol

上面这五种原始类型的值,都有对应的包装对象(wrapper object)。

运行时自动装箱,省去了将原始类型的值手动转成对象实例的麻烦。

五种包装对象之中,symbol 类型和 bigint 类型无法直接获取它们的包装对象(即Symbol()BigInt()不能作为构造函数使用),但是剩下三种可以作为构造函数调用,执行后可以直接获取某个原始类型值的包装对象。

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

由于包装对象的存在,导致每一个原始类型的值都有包装对象和原始字面量两种情况。

ts
"hello"; // 原始字面量 ‘string’
new String("hello"); // 包装对象  ‘String’

上面示例中,第一行是原始字面量,第二行是包装对象,它们都是字符串。

为了区分这两种情况,TypeScript 对五种原始类型分别提供了大写和小写两种类型。

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

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

ts
let s = '123' // string 类型

let s2 = new String("123") // String 类型

let s3:String = '123' // String 类型

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

Object 类型与 object 类型

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

Object 类型

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

ts
let obj: Object;

obj = undefined // 报错
obj = null // 报错

// 正确
obj = true;
obj = "hi";
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a: number) => a + 1;

另外,空对象{}Object类型的简写形式,所以使用Object时常常用空对象代替。实际上Object都跟 any 差不多了,用的很少。

object 类型

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

ts
let obj: object;

obj = { foo: 123 };
obj = [1, 2];
obj = (a: number) => a + 1;
obj = true; // 报错
obj = "hi"; // 报错
obj = 1; // 报错

上面示例中,object类型不包含原始类型值,只包含对象、数组和函数。

大多数时候,我们使用对象类型,只希望包含真正的对象,不希望包含原始类型。所以,建议总是使用小写类型object,不使用大写类型Object

字面量类型 Literal Type

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

TypeScript 推断类型时,遇到const命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。

ts
// x 的类型是 "https"
const x = "https";

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

注意,const命令声明的变量,如果赋值为对象,并不会推断为值类型。

ts
// x 的类型是 { foo: number }
const x = { foo: 1 };
ts
const x: 5 = 4 + 1; // 报错, 右侧的返回值是 number 类型

联合类型

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

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

ts
let x: string | number;

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

类型缩小

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

解决方法就是对参数id做一下类型缩小,确定它的类型以后再进行处理。

ts
function printId(id: number | string) {
  if (typeof id === "string") {
    console.log(id.toUpperCase());
  } else {
    console.log(id);
  }
}

“类型缩小”是 TypeScript 处理联合类型的标准方法,凡是遇到可能为多种类型的场合,都需要先缩小类型,再进行处理。

实际上,联合类型本身可以看成是一种“类型放大”(type widening),处理时就需要“类型缩小”(type narrowing)。

下面是“类型缩小”的另一个例子。

ts
function getPort(scheme: "http" | "https") {
  switch (scheme) {
    case "http":
      return 80;
    case "https":
      return 443;
  }
}

交叉类型

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

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

ts
let x: number & string;

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

交叉类型的主要用途是对象的合成。

ts
let obj: { foo: string } & { bar: string };

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

交叉类型常常用来为对象类型添加新属性

ts
type A = { foo: number };

type B = A & { bar: number };

type 命令

type命令用来定义一个类型的别名(Type Aliases)。

ts
type Age = number;

let age: Age = 55;

上面示例中,type命令为number类型定义了一个别名Age。这样就能像使用number一样,使用Age作为类型。

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

type 类型别名有那么几个特点:

  • 同作用域下,不允许重名
  • 块级作用域

typeof 运算符

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

js
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 类型。

ts
function foo<T extends number | string>(a: T): T {
    return a
}
let a: number = 1
let b: string = '123'
let ans = foo(a)
let ans2 = foo(b)

console.log(typeof ans) // "number" , js 的 typeof 运算
type myType = typeof ans2 // type myType = string , ts 的 typeof
let c:myType = 'hello'
console.log(typeof c) // "string",  js 的 typeof 运算

由于编译时不会进行 JavaScript 的值运算,所以 TypeScript 规定,typeof 的参数只能是标识符,不能是需要运算的表达式

ts
type T = typeof Date(); // 报错

另外,typeof命令的参数不能是类型。

ts
type Age = number;
type MyAge = typeof Age; // 报错

typeof 是一个很重要的 TypeScript 运算符,有些场合不知道某个变量foo的类型,这时使用typeof foo就可以获得它的类型。

类型的兼容

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

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

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

ts
let a: "hi" = "hi";
let b: string = "hello";

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