Skip to content

必须掌握的一些ts类型

any 类型

基本含义

any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。

typescript
let x:any;

x = 1; // 正确
x = 'foo'; // 正确
x = true; // 正确

上面示例中,变量x的类型是any,就可以被赋值为任意类型的值。

变量类型一旦设为any,TypeScript 实际上会关闭这个变量的类型检查。即使有明显的类型错误,只要句法正确,都不会报错。

typescript
let x:any = 'hello';

x(1) // 不报错
x.foo = 100; // 不报错

上面示例中,变量x的值是一个字符串,但是把它当作函数调用,或者当作对象读取任意属性,TypeScript 编译时都不报错。原因就是x的类型是any,TypeScript 不对其进行类型检查。

由于这个原因,应该尽量避免使用any类型,否则就失去了使用 TypeScript 的意义。

实际开发中,any类型主要适用以下两个场合。

(1)出于特殊原因,需要关闭某些变量的类型检查,就可以把该变量的类型设为any

(2)为了适配以前老的 JavaScript 项目,让代码快速迁移到 TypeScript,可以把变量类型设为any。有些年代很久的大型 JavaScript 项目,尤其是别人的代码,很难为每一行适配正确的类型,这时你为那些类型复杂的变量加上any,TypeScript 编译时就不会报错。

总之,TypeScript 认为,只要开发者使用了any类型,就表示开发者想要自己来处理这些代码,所以就不对any类型进行任何限制,怎么使用都可以。

从集合论的角度看,any类型可以看成是所有其他类型的全集,包含了一切可能的类型。TypeScript 将这种类型称为“顶层类型”(top type),意为涵盖了所有下层。

类型推断问题

对于开发者没有指定类型、TypeScript 必须自己推断类型的那些变量,如果无法推断出类型,TypeScript 就会认为该变量的类型是any

typescript
function add(x, y) {
  return x + y;
}

add(1, [1, 2, 3]) // 不报错

上面示例中,函数add()的参数变量xy,都没有足够的信息,TypeScript 无法推断出它们的类型,就会认为这两个变量和函数返回值的类型都是any。以至于后面就不再对函数add()进行类型检查了,怎么用都可以。

这显然是很糟糕的情况,所以对于那些类型不明显的变量,一定要显式声明类型,防止被推断为any

基本类型

概述

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

  • boolean
  • string
  • number
  • bigint
  • symbol
  • object
  • undefined
  • null

TypeScript 继承了 JavaScript 的类型设计,以上8种类型可以看作 TypeScript 的基本类型。

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

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

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

以下是它们的简单介绍。

boolean 类型

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

typescript
const x:boolean = true;
const y:boolean = false;

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

string 类型

string类型包含所有字符串。

typescript
const x:string = 'hello';
const y:string = `${x} world`;

上面示例中,普通字符串和模板字符串都属于 string 类型。

number 类型

number类型包含所有整数和浮点数。

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

上面示例中,整数、浮点数和非十进制数都属于 number 类型。

bigint 类型

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

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

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

bigint 与 number 类型不兼容。

typescript
const x:bigint = 123; // 报错
const y:bigint = 3.14; // 报错

上面示例中,bigint类型赋值为整数和小数,都会报错。

注意,bigint 类型是 ES2020 标准引入的。如果使用这个类型,TypeScript 编译的目标 JavaScript 版本不能低于 ES2020(即编译参数target不低于es2020)。

symbol 类型

symbol 类型包含所有的 Symbol 值。

typescript
const x:symbol = Symbol();

上面示例中,Symbol()函数的返回值就是 symbol 类型。

object 类型

根据 JavaScript 的设计,object 类型包含了所有对象、数组和函数。

typescript
const x:object = { foo: 123 };
const y:object = [1, 2, 3];
const z:object = (n:number) => n + 1;

上面示例中,对象、数组、函数都属于 object 类型。

undefined 类型,null 类型

undefined 和 null 是两种独立类型,它们各自都只有一个值。

undefined 类型只包含一个值undefined,表示未定义(即还未给出定义,以后可能会有定义)。

typescript
let x:undefined = undefined;

上面示例中,变量x就属于 undefined 类型。两个undefined里面,第一个是类型,第二个是值。

null 类型也只包含一个值null,表示为空(即此处没有值)。

typescript
const x:null = null;

上面示例中,变量x就属于 null 类型。

Object 类型与 object 类型

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

Object 类型

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

typescript
let obj:Object;
 
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;

上面示例中,原始类型值、对象、数组、函数都是合法的Object类型。

事实上,除了undefinednull这两个值不能转为对象,其他任何值都可以赋值给Object类型。

typescript
let obj:Object;

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

上面示例中,undefinednull赋值给Object类型,就会报错。

另外,空对象{}Object类型的简写形式,所以使用Object时常常用空对象代替。

typescript
let obj:{};
 
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;

上面示例中,变量obj的类型是空对象{},就代表Object类型。

显然,无所不包的Object类型既不符合直觉,也不方便使用。

object 类型

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

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

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

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

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

typescript
const o1:Object = { foo: 0 };
const o2:object = { foo: 0 };

o1.toString() // 正确
o1.foo // 报错

o2.toString() // 正确
o2.foo // 报错

上面示例中,toString()是对象的原生方法,可以正确访问。foo是自定义属性,访问就会报错。

值类型

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

typescript
let x:'hello';

x = 'hello'; // 正确
x = 'world'; // 报错

上面示例中,变量x的类型是字符串hello,导致它只能赋值为这个字符串,赋值为其他字符串就会报错。

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

typescript
// x 的类型是 "https"
const x = 'https';

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

上面示例中,变量xconst命令声明的,TypeScript 就会推断它的类型是值https,而不是string类型。

这样推断是合理的,因为const命令声明的变量,一旦声明就不能改变,相当于常量。值类型就意味着不能赋为其他值。

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

typescript
// x 的类型是 { foo: number }
const x = { foo: 1 };

上面示例中,变量x没有被推断为值类型,而是推断属性foo的类型是number。这是因为 JavaScript 里面,const变量赋值为对象时,属性值是可以改变的。

值类型可能会出现一些很奇怪的报错。

typescript
const x:5 = 4 + 1; // 报错

上面示例中,等号左侧的类型是数值5,等号右侧4 + 1的类型,TypeScript 推测为number。由于5number的子类型,number5的父类型,父类型不能赋值给子类型,所以报错了(详见本章后文)。

但是,反过来是可以的,子类型可以赋值给父类型。

typescript
let x:5 = 5;
let y:number = 4 + 1;

x = y; // 报错
y = x; // 正确

上面示例中,变量x属于子类型,变量y属于父类型。子类型x不能赋值为父类型y,但是反过来是可以的。

如果一定要让子类型可以赋值为父类型的值,就要用到类型断言(详见《类型断言》一章)。

typescript
const x:5 = (4 + 1) as 5; // 正确

上面示例中,在4 + 1后面加上as 5,就是告诉编译器,可以把4 + 1的类型视为值类型5,这样就不会报错了。

只包含单个值的值类型,用处不大。实际开发中,往往将多个值结合,作为联合类型使用。

联合类型

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

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

typescript
let x:string|number;

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

上面示例中,变量x就是联合类型string|number,表示它的值既可以是字符串,也可以是数值。

联合类型可以与值类型相结合,表示一个变量的值有若干种可能。

typescript
let Color:'赤'|'橙'|'黄'|'绿'|'青'|'蓝'|'紫';

上面的示例都是由值类型组成的联合类型,非常清晰地表达了变量的取值范围。其中,true|false其实就是布尔类型boolean

交叉类型

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

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

typescript
let x:number&string;

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

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

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

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

上面示例中,变量obj同时具有属性foo和属性bar

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

typescript
type A = { foo: number };

type B = A & { bar: number };

上面示例中,类型B是一个交叉类型,用来在A的基础上增加了属性bar

type 命令

type命令用来定义一个类型的别名。

typescript
type Age = number;

let age:Age = 55;

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

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

别名不允许重名。

typescript
type Color = 'red';
type Color = 'blue'; // 报错

上面示例中,同一个别名Color声明了两次,就报错了。

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

typescript
type Color = 'red';

if (Math.random() < 0.5) {
  type Color = 'blue';
}

上面示例中,if代码块内部的类型别名Color,跟外部的Color是不一样的。

别名支持使用表达式,也可以在定义一个别名时,使用另一个别名,即别名允许嵌套。

typescript
type World = "world";
type Greeting = `hello ${World}`;

上面示例中,别名Greeting使用了模板字符串,读取另一个别名World

type命令属于类型相关的代码,编译成 JavaScript 的时候,会被全部删除。

函数

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

typescript
function hello(txt:string):void {
  console.log('hello ' + txt);
}

上面示例中,函数hello()在声明时,需要给出参数txt的类型(string),以及返回值的类型(void),后者写在参数列表的圆括号后面。void类型表示没有返回值,详见后文。

如果不指定参数类型(比如上例不写txt的类型),TypeScript 就会推断参数类型,如果缺乏足够信息,就会推断该参数的类型为any

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

typescript
function hello(txt:string) {
  console.log('hello ' + txt);
}

上面示例中,由于没有return语句,TypeScript 会推断出函数hello()没有返回值。

不过,有时候出于文档目的,或者为了防止不小心改掉返回值,还是会写返回值的类型。

如果变量被赋值为一个函数,变量的类型有两种写法。

typescript
// 写法一
const hello = function (txt:string) {
  console.log('hello ' + txt);
}

// 写法二
const hello:
  (txt:string) => void
= function (txt) {
  console.log('hello ' + txt);
};

上面示例中,变量hello被赋值为一个函数,它的类型有两种写法。写法一是通过等号右边的函数类型,推断出变量hello的类型;写法二则是使用箭头函数的形式,为变量hello指定类型,参数的类型写在箭头左侧,返回值的类型写在箭头右侧。

写法二有两个地方需要注意。

首先,函数的参数要放在圆括号里面,不放会报错。

其次,类型里面的参数名(本例是txt)是必须的。有的语言的函数类型可以不写参数名(比如 C 语言),但是 TypeScript 不行。如果写成(string) => void,TypeScript 会理解成函数有一个名叫 string 的参数,并且这个string参数的类型是any

typescript
type MyFunc = (string, number) => number;
// (string: any, number: any) => number

上面示例中,函数类型没写参数名,导致 TypeScript 认为参数类型都是any

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

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

上面示例中,函数类型里面的参数名为x,实际的函数定义里面,参数名为y,两者并不相同。

如果函数的类型定义很冗长,或者多个函数使用同一种类型,写法二用起来就很麻烦。因此,往往用type命令为函数类型定义一个别名,便于指定给其他变量。

typescript
type MyFunc = (txt:string) => void;

const hello:MyFunc = function (txt) {
  console.log('hello ' + txt);
};

上面示例中,type命令为函数类型定义了一个别名MyFunc,后面使用就很方便,变量可以指定为这个类型。

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

typescript
let myFunc:
  (a:number, b:number) => number;

myFunc = (a:number) => a; // 正确

myFunc = (
  a:number, b:number, c:number
) => a + b + c; // 报错

上面示例中,变量myFunc的类型只能接受两个参数,如果被赋值为只有一个参数的函数,并不报错。但是,被赋值为有三个参数的函数,就会报错。

这是因为 JavaScript 函数在声明时往往有多余的参数,实际使用时可以只传入一部分参数。比如,数组的forEach()方法的参数是一个函数,该函数默认有三个参数(item, index, array) => void,实际上往往只使用第一个参数(item) => void。因此,TypeScript 允许函数传入的参数不足。

typescript
let x = (a:number) => 0;
let y = (b:number, s:string) => 0;

y = x; // 正确
x = y; // 报错

上面示例中,函数x只有一个参数,函数y有两个参数,x可以赋值给y,反过来就不行。

箭头函数

箭头函数是普通函数的一种简化写法,它的类型写法与普通函数类似。

typescript
const repeat = (str:string,times:number):string => str.repeat(times);

上面示例中,变量repeat被赋值为一个箭头函数,类型声明写在箭头函数的定义里面。其中,参数的类型写在参数名后面,返回值类型写在参数列表的圆括号后面。

可选参数

如果函数的某个参数可以省略,则在参数名后面加问号表示。

typescript
function f(x?:number) {
  // ...
}

f(); // OK
f(10); // OK

上面示例中,参数x后面有问号,表示该参数可以省略。

参数名带有问号,表示该参数的类型实际上是原始类型|undefined,它有可能为undefined。比如,上例的x虽然类型声明为number,但是实际上是number|undefined

typescript
function f(x?:number) {
  return x;
}

f(undefined) // 正确

上面示例中,参数x是可选的,等同于说x可以赋值为undefined

但是,反过来就不成立,类型显式设为undefined的参数,就不能省略。

typescript
function f(x:number|undefined) {
  return x;
}

f() // 报错

上面示例中,参数x的类型是number|undefined,表示要么传入一个数值,要么传入undefined,如果省略这个参数,就会报错。

函数的可选参数只能在参数列表的尾部,跟在必选参数的后面。

typescript
let myFunc:
  (a?:number, b:number) => number; // 报错

上面示例中,可选参数在必选参数前面,就报错了。

如果前部参数有可能为空,这时只能显式注明该参数类型可能为undefined

typescript
let myFunc:
  (
    a:number|undefined,
    b:number
  ) => number;

上面示例中,参数a有可能为空,就只能显式注明类型包括undefined,传参时也要显式传入undefined

函数体内部用到可选参数时,需要判断该参数是否为undefined

typescript
let myFunc:
  (a:number, b?:number) => number; 

myFunc = function (x, y) {
  if (y === undefined) {
    return x;
  }
  return x + y;
}

上面示例中,由于函数的第二个参数为可选参数,所以函数体内部需要判断一下,该参数是否为空。

复杂类型

数组

数组的类型有两种写法。

第一种写法是在数组成员的类型后面,加上一对方括号。

typescript
let arr:number[] = [1, 2, 3];

上面示例中,数组arr的类型是number[],其中number表示数组成员类型是number

数组类型的第二种写法是使用 TypeScript 内置的 Array 接口。

typescript
let arr:Array<number> = [1, 2, 3];

上面示例中,数组arr的类型是Array<number>,其中number表示成员类型是number

这种写法本质上属于泛型,这里只要知道怎么写就可以了,后面我们讲vue的组件封装时会常用泛型写法

对象

除了原始类型,对象是 JavaScript 最基本的数据结构。TypeScript 对于对象类型有很多规则。

对象类型的最简单声明方法,就是使用大括号表示对象,在大括号内部声明每个属性和方法的类型。

typescript
const obj:{
  x:number;
  y:number;
} = { x: 1, y: 1 };

上面示例中,对象obj的类型就写在变量名后面,使用大括号描述,内部声明每个属性的属性名和类型。

属性的类型可以用分号结尾,也可以用逗号结尾。

typescript
// 属性类型以分号结尾
type MyObj = {
  x:number;
  y:number;
};

// 属性类型以逗号结尾
type MyObj = {
  x:number,
  y:number,
};

最后一个属性后面,可以写分号或逗号,也可以不写。

一旦声明了类型,对象赋值时,就不能缺少指定的属性,也不能有多余的属性。

typescript
type MyObj = {
  x:number;
  y:number;
};

const o1:MyObj = { x: 1 }; // 报错
const o2:MyObj = { x: 1, y: 1, z: 1 }; // 报错

上面示例中,变量o1缺少了属性y,变量o2多出了属性z,都会报错。

读写不存在的属性也会报错。

typescript
const obj:{
  x:number;
  y:number;
} = { x: 1, y: 1 };

console.log(obj.z); // 报错
obj.z = 1; // 报错

上面示例中,读写不存在的属性z都会报错。

同样地,也不能删除类型声明中存在的属性,修改属性值是可以的。

typescript
const myUser = {
  name: "Sabrina",
};

delete myUser.name // 报错
myUser.name = "Cynthia"; // 正确

上面声明中,删除类型声明中存在的属性name会报错,但是可以修改它的值。

对象的方法使用函数类型描述。

typescript
const obj:{
  x: number;
  y: number;
  add(x:number, y:number): number;
  // 或者写成
  // add: (x:number, y:number) => number;
} = {
  x: 1,
  y: 1,
  add(x, y) {
    return x + y;
  }
};

上面示例中,对象obj有一个方法add(),需要定义它的参数类型和返回值类型。

对象类型可以使用方括号读取属性的类型。

typescript
type User = {
  name: string,
  age: number
};
type Name = User['name']; // string

上面示例中,对象类型User使用方括号,读取了属性name的类型(string)。

除了type命令可以为对象类型声明一个别名,TypeScript 还提供了interface命令,可以把对象类型提炼为一个接口。

typescript
// 写法一
type MyObj = {
  x:number;
  y:number;
};

const obj:MyObj = { x: 1, y: 1 };

// 写法二
interface MyObj {
  x: number;
  y: number;
}

const obj:MyObj = { x: 1, y: 1 };

上面示例中,写法一是type命令的用法,写法二是interface命令的用法。interface命令的详细解释,以及与type命令的区别,会在下文中提到

可选属性

如果某个属性是可选的(即可以忽略),需要在属性名后面加一个问号。

typescript
const obj: {
  x: number;
  y?: number;
} = { x: 1 };

上面示例中,属性y是可选的。

可选属性等同于允许赋值为undefined,下面两种写法是等效的。

typescript
type User = {
  firstName: string;
  lastName?: string;
};

// 等同于
type User = {
  firstName: string;
  lastName?: string|undefined;
};

上面示例中,类型User的可选属性lastName可以是字符串,也可以是undefined,即可选属性可以赋值为undefined

typescript
const obj: {
  x: number;
  y?: number;
} = { x: 1, y: undefined };

上面示例中,可选属性y赋值为undefined,不会报错。

同样地,读取一个没有赋值的可选属性时,返回undefined

typescript
type MyObj = {
  x: string,
  y?: string
};

const obj:MyObj = { x: 'hello' };
obj.y.toLowerCase() // 报错

上面示例中,最后一行会报错,因为obj.y返回undefined,无法对其调用toLowerCase()

所以,读取可选属性之前,必须检查一下是否为undefined

typescript
const user:{
  firstName: string;
  lastName?: string;
} = { firstName: 'Foo'};

if (user.lastName !== undefined) {
  console.log(`hello ${user.firstName} ${user.lastName}`)
}

属性名的索引类型

如果对象的属性非常多,一个个声明类型就很麻烦,而且有些时候,无法事前知道对象会有多少属性,比如外部 API 返回的对象。这时 TypeScript 允许采用属性名表达式的写法来描述类型,称为“属性名的索引类型”。

索引类型里面,最常见的就是属性名的字符串索引。

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

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

上面示例中,类型MyObj的属性名类型就采用了表达式形式,写在方括号里面。[property: string]property表示属性名,这个是可以随便起的,它的类型是string,即属性名类型为string。也就是说,不管这个对象有多少属性,只要属性名为字符串,且属性值也是字符串,就符合这个类型声明。

大多数的项目一般都写成

typescript
type MyObj = {
  [key: string]: string
};

JavaScript 对象的属性名(即上例的property)的类型有三种可能,除了上例的string,还有numbersymbol

typescript
type T1 = {
  [property: number]: string
};

type T2 = {
  [property: symbol]: string
};

上面示例中,对象属性名的类型分别为numbersymbol

typescript
type MyArr = {
  [n:number]: number;
};

const arr:MyArr = [1, 2, 3];
// 或者
const arr:MyArr = {
  0: 1,
  1: 2,
  2: 3,
};

上面示例中,对象类型MyArr的属性名是[n:number],就表示它的属性名都是数值,比如012

对象可以同时有多种类型的属性名索引,比如同时有数值索引和字符串索引。但是,数值索引不能与字符串索引发生冲突,必须服从后者,这是因为在 JavaScript 语言内部,所有的数值属性名都会自动转为字符串属性名。

typescript
type MyType = {
  [x: number]: boolean; // 报错
  [x: string]: string;
}

上面示例中,类型MyType同时有两种属性名索引,但是数值索引与字符串索引冲突了,所以报错了。由于字符属性名的值类型是string,数值属性名的值类型只有同样为string,才不会报错。

同样地,可以既声明属性名索引,也声明具体的单个属性名。如果单个属性名不符合属性名索引的范围,两者发生冲突,就会报错。

typescript
type MyType = {
  foo: boolean; // 报错
  [x: string]: string;
}

上面示例中,属性名foo符合属性名的字符串索引,但是两者的属性值类型不一样,所以报错了。

属性的索引类型写法,建议谨慎使用,因为属性名的声明太宽泛,约束太少。另外,属性名的数值索引不宜用来声明数组,因为采用这种方式声明数组,就不能使用各种数组方法以及length属性,因为类型里面没有定义这些东西。

typescript
type MyArr = {
  [n:number]: number;
};

const arr:MyArr = [1, 2, 3];
arr.length // 报错

上面示例中,读取arr.length属性会报错,因为类型MyArr没有这个属性。

Record<Keys, Type>

除了使用索引类型的写法定义对象,还可以使用Record来进行类型的定义,以下两种写法是等价的

typescript
type MyObj = {
  [key: string]: string
};
type MyObj = Record<string,string>;

interface

interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。使用了某个模板的对象,就拥有了指定的类型结构。

typescript
interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

上面示例中,定义了一个接口Person,它指定一个对象模板,拥有三个属性firstNamelastNameage。任何实现这个接口的对象,都必须部署这三个属性,并且必须符合规定的类型。

实现该接口很简单,只要指定它作为对象的类型即可。

typescript
const p:Person = {
  firstName: 'John',
  lastName: 'Smith',
  age: 25
};

上面示例中,变量p的类型就是接口Person,所以必须符合Person指定的结构。

方括号运算符可以取出 interface 某个属性的类型。

typescript
interface Foo {
  a: string;
}

type A = Foo['a']; // string

上面示例中,Foo['a']返回属性a的类型,所以类型A就是string

interface 可以表示对象的各种语法

对象属性

typescript
interface Point {
  x: number;
  y: number;
}

上面示例中,xy都是对象的属性,分别使用冒号指定每个属性的类型。

属性之间使用分号或逗号分隔,最后一个属性结尾的分号或逗号可以省略。

如果属性是可选的,就在属性名后面加一个问号。

typescript
interface Foo {
  x?: string;
}

对象的属性索引

typescript
interface A {
  [prop: string]: number;
}

上面示例中,[prop: string]就是属性的字符串索引,表示属性名只要是字符串,都符合类型要求。

属性索引共有stringnumbersymbol三种类型。

一个接口中,最多只能定义一个字符串索引。字符串索引会约束该类型中所有名字为字符串的属性。

typescript
interface MyObj {
  [prop: string]: number;

  a: boolean;      // 编译错误
}

上面示例中,属性索引指定所有名称为字符串的属性,它们的属性值必须是数值(number)。属性a的值为布尔值就报错了。

属性的数值索引

其实是指定数组的类型。

typescript
interface A {
  [prop: number]: string;
}

const obj:A = ['a', 'b', 'c'];

上面示例中,[prop: number]表示属性名的类型是数值,所以可以用数组对变量obj赋值。

同样的,一个接口中最多只能定义一个数值索引。数值索引会约束所有名称为数值的属性。

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

typescript
interface A {
  [prop: string]: number;
  [prop: number]: string; // 报错
}

interface B {
  [prop: string]: number;
  [prop: number]: number; // 正确
}

上面示例中,数值索引的属性值类型与字符串索引不一致,就会报错。数值索引必须兼容字符串索引的类型声明。

函数

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

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

const myAdd:Add = (x,y) => x + y;

上面示例中,接口Add声明了一个函数类型。

interface 的继承

interface 可以继承其他类型,主要有下面几种情况。

interface 继承 interface

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

typescript
interface Shape {
  name: string;
}

interface Circle extends Shape {
  radius: number;
}

上面示例中,Circle继承了Shape,所以其实有两个属性名称半径。这时,Circle是子接口,Shape是父接口。

extends关键字会从继承的接口里面拷贝属性类型,这样就不必书写重复的属性。

interface 允许多重继承。

typescript
interface Style {
  color: string;
}

interface Shape {
  name: string;
}

interface Circle extends Style, Shape {
  radius: number;
}

上面示例中,Circle同时继承了StyleShape,所以拥有三个属性colornameradius

多重接口继承,实际上相当于多个父接口的合并。

如果子接口与父接口存在同名属性,那么子接口的属性会覆盖父接口的属性。注意,子接口与父接口的同名属性必须是类型兼容的,不能有冲突,否则会报错。

typescript
interface Foo {
  id: string;
}

interface Bar extends Foo {
  id: number; // 报错
}

上面示例中,Bar继承了Foo,但是两者的同名属性id的类型不兼容,导致报错。

多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错。

typescript
interface Foo {
  id: string;
}

interface Bar {
  id: number;
}

// 报错
interface Baz extends Foo, Bar {
  type: string;
}

上面示例中,Baz同时继承了FooBar,但是后两者的同名属性id有类型冲突,导致报错。

interface 继承 type

interface 可以继承type命令定义的对象类型。

typescript
type Country = {
  name: string;
  capital: string;
}

interface CountryWithPop extends Country {
  population: number;
}

上面示例中,CountryWithPop继承了type命令定义的Country对象,并且新增了一个population属性。

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

接口合并

多个同名接口会合并成一个接口。

typescript
interface Box {
  height: number;
  width: number;
}

interface Box {
  length: number;
}

上面示例中,两个Box接口会合并成一个接口,同时有heightwidthlength三个属性。

这样的设计主要是为了兼容 JavaScript 的行为。JavaScript 开发者常常对全局对象或者外部库,添加自己的属性和方法。那么,只要使用 interface 给出这些自定义属性和方法的类型,就能自动跟原始的 interface 合并,使得扩展外部类型非常方便。

举例来说,Web 网页开发经常会对window对象和document对象添加自定义属性,但是 TypeScript 会报错,因为原始定义没有这些属性。解决方法就是把自定义属性写成 interface,合并进原始定义。

typescript
interface Document {
  foo: string;
}

document.foo = 'hello';

上面示例中,接口Document增加了一个自定义属性foo,从而就可以在document对象上使用自定义属性。

同名接口合并时,同一个属性如果有多个类型声明,彼此不能有类型冲突。

typescript
interface A {
  a: number;
}

interface A {
  a: string; // 报错
}

上面示例中,接口A的属性a有两个类型声明,彼此是冲突的,导致报错。

同名接口合并时,如果同名方法有不同的类型声明,那么会发生函数重载。而且,后面的定义比前面的定义具有更高的优先级。

typescript
interface Cloner {
  clone(animal: Animal): Animal;
}

interface Cloner {
  clone(animal: Sheep): Sheep;
}

interface Cloner {
  clone(animal: Dog): Dog;
  clone(animal: Cat): Cat;
}

// 等同于
interface Cloner {
  clone(animal: Dog): Dog;
  clone(animal: Cat): Cat;
  clone(animal: Sheep): Sheep;
  clone(animal: Animal): Animal;
}

上面示例中,clone()方法有不同的类型声明,会发生函数重载。这时,越靠后的定义,优先级越高,排在函数重载的越前面。比如,clone(animal: Animal)是最先出现的类型声明,就排在函数重载的最后,属于clone()函数最后匹配的类型。

这个规则有一个例外。同名方法之中,如果有一个参数是字面量类型,字面量类型有更高的优先级。

typescript
interface A {
  f(x:'foo'): boolean;
}

interface A {
  f(x:any): void;
}

// 等同于
interface A {
  f(x:'foo'): boolean;
  f(x:any): void;
}

上面示例中,f()方法有一个类型声明的参数x是字面量类型,这个类型声明的优先级最高,会排在函数重载的最前面。

interface 与 type 的异同

interface命令与type命令作用类似,都可以表示对象类型。

很多对象类型既可以用 interface 表示,也可以用 type 表示。而且,两者往往可以换用,几乎所有的 interface 命令都可以改写为 type 命令。

它们的相似之处,首先表现在都能为对象类型起名。

typescript
type Country = {
  name: string;
  capital: string;
}

interface Country {
  name: string;
  capital: string;
}

上面示例是type命令和interface命令,分别定义同一个类型。

interface 与 type 的区别有下面几点。

(1)type能够表示非对象类型,而interface只能表示对象类型(包括数组、函数等)。

(2)interface可以继承其他类型,type不支持继承。

继承的主要作用是添加属性,type定义的对象类型如果想要添加属性,只能使用&运算符,重新定义一个类型。

typescript
type Animal = {
  name: string
}

type Bear = Animal & {
  honey: boolean
}

上面示例中,类型BearAnimal的基础上添加了一个属性honey

上例的&运算符,表示同时具备两个类型的特征,因此可以起到两个对象类型合并的作用。

作为比较,interface添加属性,采用的是继承的写法。

typescript
interface Animal {
  name: string
}

interface Bear extends Animal {
  honey: boolean
}

继承时,type 和 interface 是可以换用的。interface 可以继承 type。

typescript
type Foo = { x: number; };

interface Bar extends Foo {
  y: number;
}

type 也可以继承 interface。

typescript
interface Foo {
  x: number;
}

type Bar = Foo & { y: number; };

(3)同名interface会自动合并,同名type则会报错。也就是说,TypeScript 不允许使用type多次定义同一个类型。

typescript
type A = { foo:number }; // 报错
type A = { bar:number }; // 报错

上面示例中,type两次定义了类型A,导致两行都会报错。

作为比较,interface则会自动合并。

typescript
interface A { foo:number };
interface A { bar:number };

const obj:A = {
  foo: 1,
  bar: 1
};

上面示例中,interface把类型A的两个定义合并在一起。

这表明,interface 是开放的,可以添加属性,type 是封闭的,不能添加属性,只能定义新的 type。

(4)interface无法表达某些复杂类型(比如交叉类型和联合类型),但是type可以。

type A = { /* ... */ };
type B = { /* ... */ };

type AorB = A | B;
type AorBwithName = AorB & {
  name: string
};

上面示例中,类型AorB是一个联合类型,AorBwithName则是为AorB添加一个属性。这两种运算,interface都没法表达。

综上所述,如果有复杂的类型运算,那么没有其他选择只能使用type;一般情况下,interface灵活性比较高,便于扩充类型或自动合并,建议优先使用。

简单来说,就是如果你要定义的类型用interfacetype都能实现,那么就用interface