必须掌握的一些ts类型
any 类型
基本含义
any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。
let x:any;
x = 1; // 正确
x = 'foo'; // 正确
x = true; // 正确上面示例中,变量x的类型是any,就可以被赋值为任意类型的值。
变量类型一旦设为any,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。
function add(x, y) {
return x + y;
}
add(1, [1, 2, 3]) // 不报错上面示例中,函数add()的参数变量x和y,都没有足够的信息,TypeScript 无法推断出它们的类型,就会认为这两个变量和函数返回值的类型都是any。以至于后面就不再对函数add()进行类型检查了,怎么用都可以。
这显然是很糟糕的情况,所以对于那些类型不明显的变量,一定要显式声明类型,防止被推断为any。
基本类型
概述
JavaScript 语言(注意,不是 TypeScript)将值分成8种类型。
- boolean
- string
- number
- bigint
- symbol
- object
- undefined
- null
TypeScript 继承了 JavaScript 的类型设计,以上8种类型可以看作 TypeScript 的基本类型。
注意,上面所有类型的名称都是小写字母,首字母大写的Number、String、Boolean等在 JavaScript 语言中都是内置对象,而不是类型名称。
另外,undefined 和 null 既可以作为值,也可以作为类型,取决于在哪里使用它们。
这8种基本类型是 TypeScript 类型系统的基础,复杂类型由它们组合而成。
以下是它们的简单介绍。
boolean 类型
boolean类型只包含true和false两个布尔值。
const x:boolean = true;
const y:boolean = false;上面示例中,变量x和y就属于 boolean 类型。
string 类型
string类型包含所有字符串。
const x:string = 'hello';
const y:string = `${x} world`;上面示例中,普通字符串和模板字符串都属于 string 类型。
number 类型
number类型包含所有整数和浮点数。
const x:number = 123;
const y:number = 3.14;
const z:number = 0xffff;上面示例中,整数、浮点数和非十进制数都属于 number 类型。
bigint 类型
bigint 类型包含所有的大整数。
const x:bigint = 123n;
const y:bigint = 0xffffn;上面示例中,变量x和y就属于 bigint 类型。
bigint 与 number 类型不兼容。
const x:bigint = 123; // 报错
const y:bigint = 3.14; // 报错上面示例中,bigint类型赋值为整数和小数,都会报错。
注意,bigint 类型是 ES2020 标准引入的。如果使用这个类型,TypeScript 编译的目标 JavaScript 版本不能低于 ES2020(即编译参数target不低于es2020)。
symbol 类型
symbol 类型包含所有的 Symbol 值。
const x:symbol = Symbol();上面示例中,Symbol()函数的返回值就是 symbol 类型。
object 类型
根据 JavaScript 的设计,object 类型包含了所有对象、数组和函数。
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,表示未定义(即还未给出定义,以后可能会有定义)。
let x:undefined = undefined;上面示例中,变量x就属于 undefined 类型。两个undefined里面,第一个是类型,第二个是值。
null 类型也只包含一个值null,表示为空(即此处没有值)。
const x:null = null;上面示例中,变量x就属于 null 类型。
Object 类型与 object 类型
TypeScript 的对象类型也有大写Object和小写object两种。
Object 类型
大写的Object类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值,都是Object类型,这囊括了几乎所有的值。
let obj:Object;
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;上面示例中,原始类型值、对象、数组、函数都是合法的Object类型。
事实上,除了undefined和null这两个值不能转为对象,其他任何值都可以赋值给Object类型。
let obj:Object;
obj = undefined; // 报错
obj = null; // 报错上面示例中,undefined和null赋值给Object类型,就会报错。
另外,空对象{}是Object类型的简写形式,所以使用Object时常常用空对象代替。
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 里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数,不包括原始类型的值。
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 内置对象原生的属性和方法,用户自定义的属性和方法都不存在于这两个类型之中。
const o1:Object = { foo: 0 };
const o2:object = { foo: 0 };
o1.toString() // 正确
o1.foo // 报错
o2.toString() // 正确
o2.foo // 报错上面示例中,toString()是对象的原生方法,可以正确访问。foo是自定义属性,访问就会报错。
值类型
TypeScript 规定,单个值也是一种类型,称为“值类型”。
let x:'hello';
x = 'hello'; // 正确
x = 'world'; // 报错上面示例中,变量x的类型是字符串hello,导致它只能赋值为这个字符串,赋值为其他字符串就会报错。
TypeScript 推断类型时,遇到const命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。
// x 的类型是 "https"
const x = 'https';
// y 的类型是 string
const y:string = 'https';上面示例中,变量x是const命令声明的,TypeScript 就会推断它的类型是值https,而不是string类型。
这样推断是合理的,因为const命令声明的变量,一旦声明就不能改变,相当于常量。值类型就意味着不能赋为其他值。
注意,const命令声明的变量,如果赋值为对象,并不会推断为值类型。
// x 的类型是 { foo: number }
const x = { foo: 1 };上面示例中,变量x没有被推断为值类型,而是推断属性foo的类型是number。这是因为 JavaScript 里面,const变量赋值为对象时,属性值是可以改变的。
值类型可能会出现一些很奇怪的报错。
const x:5 = 4 + 1; // 报错上面示例中,等号左侧的类型是数值5,等号右侧4 + 1的类型,TypeScript 推测为number。由于5是number的子类型,number是5的父类型,父类型不能赋值给子类型,所以报错了(详见本章后文)。
但是,反过来是可以的,子类型可以赋值给父类型。
let x:5 = 5;
let y:number = 4 + 1;
x = y; // 报错
y = x; // 正确上面示例中,变量x属于子类型,变量y属于父类型。子类型x不能赋值为父类型y,但是反过来是可以的。
如果一定要让子类型可以赋值为父类型的值,就要用到类型断言(详见《类型断言》一章)。
const x:5 = (4 + 1) as 5; // 正确上面示例中,在4 + 1后面加上as 5,就是告诉编译器,可以把4 + 1的类型视为值类型5,这样就不会报错了。
只包含单个值的值类型,用处不大。实际开发中,往往将多个值结合,作为联合类型使用。
联合类型
联合类型(union types)指的是多个类型组成的一个新类型,使用符号|表示。
联合类型A|B表示,任何一个类型只要属于A或B,就属于联合类型A|B。
let x:string|number;
x = 123; // 正确
x = 'abc'; // 正确上面示例中,变量x就是联合类型string|number,表示它的值既可以是字符串,也可以是数值。
联合类型可以与值类型相结合,表示一个变量的值有若干种可能。
let Color:'赤'|'橙'|'黄'|'绿'|'青'|'蓝'|'紫';上面的示例都是由值类型组成的联合类型,非常清晰地表达了变量的取值范围。其中,true|false其实就是布尔类型boolean。
交叉类型
交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号&表示。
交叉类型A&B表示,任何一个类型必须同时属于A和B,才属于交叉类型A&B,即交叉类型同时满足A和B的特征。
let x:number&string;上面示例中,变量x同时是数值和字符串,这当然是不可能的,所以 TypeScript 会认为x的类型实际是never。
交叉类型的主要用途是表示对象的合成。
let obj:
{ foo: string } &
{ bar: string };
obj = {
foo: 'hello',
bar: 'world'
};上面示例中,变量obj同时具有属性foo和属性bar。
交叉类型常常用来为对象类型添加新属性。
type A = { foo: number };
type B = A & { bar: number };上面示例中,类型B是一个交叉类型,用来在A的基础上增加了属性bar。
type 命令
type命令用来定义一个类型的别名。
type Age = number;
let age:Age = 55;上面示例中,type命令为number类型定义了一个别名Age。这样就能像使用number一样,使用Age作为类型。
别名可以让类型的名字变得更有意义,也能增加代码的可读性,还可以使复杂类型用起来更方便,便于以后修改变量的类型。
别名不允许重名。
type Color = 'red';
type Color = 'blue'; // 报错上面示例中,同一个别名Color声明了两次,就报错了。
别名的作用域是块级作用域。这意味着,代码块内部定义的别名,影响不到外部。
type Color = 'red';
if (Math.random() < 0.5) {
type Color = 'blue';
}上面示例中,if代码块内部的类型别名Color,跟外部的Color是不一样的。
别名支持使用表达式,也可以在定义一个别名时,使用另一个别名,即别名允许嵌套。
type World = "world";
type Greeting = `hello ${World}`;上面示例中,别名Greeting使用了模板字符串,读取另一个别名World。
type命令属于类型相关的代码,编译成 JavaScript 的时候,会被全部删除。
函数
函数的类型声明,需要在声明函数时,给出参数的类型和返回值的类型。
function hello(txt:string):void {
console.log('hello ' + txt);
}上面示例中,函数hello()在声明时,需要给出参数txt的类型(string),以及返回值的类型(void),后者写在参数列表的圆括号后面。void类型表示没有返回值,详见后文。
如果不指定参数类型(比如上例不写txt的类型),TypeScript 就会推断参数类型,如果缺乏足够信息,就会推断该参数的类型为any。
返回值的类型通常可以不写,因为 TypeScript 自己会推断出来。
function hello(txt:string) {
console.log('hello ' + txt);
}上面示例中,由于没有return语句,TypeScript 会推断出函数hello()没有返回值。
不过,有时候出于文档目的,或者为了防止不小心改掉返回值,还是会写返回值的类型。
如果变量被赋值为一个函数,变量的类型有两种写法。
// 写法一
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。
type MyFunc = (string, number) => number;
// (string: any, number: any) => number上面示例中,函数类型没写参数名,导致 TypeScript 认为参数类型都是any。
函数类型里面的参数名与实际参数名,可以不一致。
let f:(x:number) => number;
f = function (y:number) {
return y;
};上面示例中,函数类型里面的参数名为x,实际的函数定义里面,参数名为y,两者并不相同。
如果函数的类型定义很冗长,或者多个函数使用同一种类型,写法二用起来就很麻烦。因此,往往用type命令为函数类型定义一个别名,便于指定给其他变量。
type MyFunc = (txt:string) => void;
const hello:MyFunc = function (txt) {
console.log('hello ' + txt);
};上面示例中,type命令为函数类型定义了一个别名MyFunc,后面使用就很方便,变量可以指定为这个类型。
函数的实际参数个数,可以少于类型指定的参数个数,但是不能多于,即 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 允许函数传入的参数不足。
let x = (a:number) => 0;
let y = (b:number, s:string) => 0;
y = x; // 正确
x = y; // 报错上面示例中,函数x只有一个参数,函数y有两个参数,x可以赋值给y,反过来就不行。
箭头函数
箭头函数是普通函数的一种简化写法,它的类型写法与普通函数类似。
const repeat = (str:string,times:number):string => str.repeat(times);上面示例中,变量repeat被赋值为一个箭头函数,类型声明写在箭头函数的定义里面。其中,参数的类型写在参数名后面,返回值类型写在参数列表的圆括号后面。
可选参数
如果函数的某个参数可以省略,则在参数名后面加问号表示。
function f(x?:number) {
// ...
}
f(); // OK
f(10); // OK上面示例中,参数x后面有问号,表示该参数可以省略。
参数名带有问号,表示该参数的类型实际上是原始类型|undefined,它有可能为undefined。比如,上例的x虽然类型声明为number,但是实际上是number|undefined。
function f(x?:number) {
return x;
}
f(undefined) // 正确上面示例中,参数x是可选的,等同于说x可以赋值为undefined。
但是,反过来就不成立,类型显式设为undefined的参数,就不能省略。
function f(x:number|undefined) {
return x;
}
f() // 报错上面示例中,参数x的类型是number|undefined,表示要么传入一个数值,要么传入undefined,如果省略这个参数,就会报错。
函数的可选参数只能在参数列表的尾部,跟在必选参数的后面。
let myFunc:
(a?:number, b:number) => number; // 报错上面示例中,可选参数在必选参数前面,就报错了。
如果前部参数有可能为空,这时只能显式注明该参数类型可能为undefined。
let myFunc:
(
a:number|undefined,
b:number
) => number;上面示例中,参数a有可能为空,就只能显式注明类型包括undefined,传参时也要显式传入undefined。
函数体内部用到可选参数时,需要判断该参数是否为undefined。
let myFunc:
(a:number, b?:number) => number;
myFunc = function (x, y) {
if (y === undefined) {
return x;
}
return x + y;
}上面示例中,由于函数的第二个参数为可选参数,所以函数体内部需要判断一下,该参数是否为空。
复杂类型
数组
数组的类型有两种写法。
第一种写法是在数组成员的类型后面,加上一对方括号。
let arr:number[] = [1, 2, 3];上面示例中,数组arr的类型是number[],其中number表示数组成员类型是number。
数组类型的第二种写法是使用 TypeScript 内置的 Array 接口。
let arr:Array<number> = [1, 2, 3];上面示例中,数组arr的类型是Array<number>,其中number表示成员类型是number。
这种写法本质上属于泛型,这里只要知道怎么写就可以了,后面我们讲vue的组件封装时会常用泛型写法
对象
除了原始类型,对象是 JavaScript 最基本的数据结构。TypeScript 对于对象类型有很多规则。
对象类型的最简单声明方法,就是使用大括号表示对象,在大括号内部声明每个属性和方法的类型。
const obj:{
x:number;
y:number;
} = { x: 1, y: 1 };上面示例中,对象obj的类型就写在变量名后面,使用大括号描述,内部声明每个属性的属性名和类型。
属性的类型可以用分号结尾,也可以用逗号结尾。
// 属性类型以分号结尾
type MyObj = {
x:number;
y:number;
};
// 属性类型以逗号结尾
type MyObj = {
x:number,
y:number,
};最后一个属性后面,可以写分号或逗号,也可以不写。
一旦声明了类型,对象赋值时,就不能缺少指定的属性,也不能有多余的属性。
type MyObj = {
x:number;
y:number;
};
const o1:MyObj = { x: 1 }; // 报错
const o2:MyObj = { x: 1, y: 1, z: 1 }; // 报错上面示例中,变量o1缺少了属性y,变量o2多出了属性z,都会报错。
读写不存在的属性也会报错。
const obj:{
x:number;
y:number;
} = { x: 1, y: 1 };
console.log(obj.z); // 报错
obj.z = 1; // 报错上面示例中,读写不存在的属性z都会报错。
同样地,也不能删除类型声明中存在的属性,修改属性值是可以的。
const myUser = {
name: "Sabrina",
};
delete myUser.name // 报错
myUser.name = "Cynthia"; // 正确上面声明中,删除类型声明中存在的属性name会报错,但是可以修改它的值。
对象的方法使用函数类型描述。
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(),需要定义它的参数类型和返回值类型。
对象类型可以使用方括号读取属性的类型。
type User = {
name: string,
age: number
};
type Name = User['name']; // string上面示例中,对象类型User使用方括号,读取了属性name的类型(string)。
除了type命令可以为对象类型声明一个别名,TypeScript 还提供了interface命令,可以把对象类型提炼为一个接口。
// 写法一
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命令的区别,会在下文中提到
可选属性
如果某个属性是可选的(即可以忽略),需要在属性名后面加一个问号。
const obj: {
x: number;
y?: number;
} = { x: 1 };上面示例中,属性y是可选的。
可选属性等同于允许赋值为undefined,下面两种写法是等效的。
type User = {
firstName: string;
lastName?: string;
};
// 等同于
type User = {
firstName: string;
lastName?: string|undefined;
};上面示例中,类型User的可选属性lastName可以是字符串,也可以是undefined,即可选属性可以赋值为undefined。
const obj: {
x: number;
y?: number;
} = { x: 1, y: undefined };上面示例中,可选属性y赋值为undefined,不会报错。
同样地,读取一个没有赋值的可选属性时,返回undefined。
type MyObj = {
x: string,
y?: string
};
const obj:MyObj = { x: 'hello' };
obj.y.toLowerCase() // 报错上面示例中,最后一行会报错,因为obj.y返回undefined,无法对其调用toLowerCase()。
所以,读取可选属性之前,必须检查一下是否为undefined。
const user:{
firstName: string;
lastName?: string;
} = { firstName: 'Foo'};
if (user.lastName !== undefined) {
console.log(`hello ${user.firstName} ${user.lastName}`)
}属性名的索引类型
如果对象的属性非常多,一个个声明类型就很麻烦,而且有些时候,无法事前知道对象会有多少属性,比如外部 API 返回的对象。这时 TypeScript 允许采用属性名表达式的写法来描述类型,称为“属性名的索引类型”。
索引类型里面,最常见的就是属性名的字符串索引。
type MyObj = {
[property: string]: string
};
const obj:MyObj = {
foo: 'a',
bar: 'b',
baz: 'c',
};上面示例中,类型MyObj的属性名类型就采用了表达式形式,写在方括号里面。[property: string]的property表示属性名,这个是可以随便起的,它的类型是string,即属性名类型为string。也就是说,不管这个对象有多少属性,只要属性名为字符串,且属性值也是字符串,就符合这个类型声明。
大多数的项目一般都写成
type MyObj = {
[key: string]: string
};JavaScript 对象的属性名(即上例的property)的类型有三种可能,除了上例的string,还有number和symbol。
type T1 = {
[property: number]: string
};
type T2 = {
[property: symbol]: string
};上面示例中,对象属性名的类型分别为number和symbol。
type MyArr = {
[n:number]: number;
};
const arr:MyArr = [1, 2, 3];
// 或者
const arr:MyArr = {
0: 1,
1: 2,
2: 3,
};上面示例中,对象类型MyArr的属性名是[n:number],就表示它的属性名都是数值,比如0、1、2。
对象可以同时有多种类型的属性名索引,比如同时有数值索引和字符串索引。但是,数值索引不能与字符串索引发生冲突,必须服从后者,这是因为在 JavaScript 语言内部,所有的数值属性名都会自动转为字符串属性名。
type MyType = {
[x: number]: boolean; // 报错
[x: string]: string;
}上面示例中,类型MyType同时有两种属性名索引,但是数值索引与字符串索引冲突了,所以报错了。由于字符属性名的值类型是string,数值属性名的值类型只有同样为string,才不会报错。
同样地,可以既声明属性名索引,也声明具体的单个属性名。如果单个属性名不符合属性名索引的范围,两者发生冲突,就会报错。
type MyType = {
foo: boolean; // 报错
[x: string]: string;
}上面示例中,属性名foo符合属性名的字符串索引,但是两者的属性值类型不一样,所以报错了。
属性的索引类型写法,建议谨慎使用,因为属性名的声明太宽泛,约束太少。另外,属性名的数值索引不宜用来声明数组,因为采用这种方式声明数组,就不能使用各种数组方法以及length属性,因为类型里面没有定义这些东西。
type MyArr = {
[n:number]: number;
};
const arr:MyArr = [1, 2, 3];
arr.length // 报错上面示例中,读取arr.length属性会报错,因为类型MyArr没有这个属性。
Record<Keys, Type>
除了使用索引类型的写法定义对象,还可以使用Record来进行类型的定义,以下两种写法是等价的
type MyObj = {
[key: string]: string
};
type MyObj = Record<string,string>;interface
interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。使用了某个模板的对象,就拥有了指定的类型结构。
interface Person {
firstName: string;
lastName: string;
age: number;
}上面示例中,定义了一个接口Person,它指定一个对象模板,拥有三个属性firstName、lastName和age。任何实现这个接口的对象,都必须部署这三个属性,并且必须符合规定的类型。
实现该接口很简单,只要指定它作为对象的类型即可。
const p:Person = {
firstName: 'John',
lastName: 'Smith',
age: 25
};上面示例中,变量p的类型就是接口Person,所以必须符合Person指定的结构。
方括号运算符可以取出 interface 某个属性的类型。
interface Foo {
a: string;
}
type A = Foo['a']; // string上面示例中,Foo['a']返回属性a的类型,所以类型A就是string。
interface 可以表示对象的各种语法
对象属性
interface Point {
x: number;
y: number;
}上面示例中,x和y都是对象的属性,分别使用冒号指定每个属性的类型。
属性之间使用分号或逗号分隔,最后一个属性结尾的分号或逗号可以省略。
如果属性是可选的,就在属性名后面加一个问号。
interface Foo {
x?: string;
}对象的属性索引
interface A {
[prop: string]: number;
}上面示例中,[prop: string]就是属性的字符串索引,表示属性名只要是字符串,都符合类型要求。
属性索引共有string、number和symbol三种类型。
一个接口中,最多只能定义一个字符串索引。字符串索引会约束该类型中所有名字为字符串的属性。
interface MyObj {
[prop: string]: number;
a: boolean; // 编译错误
}上面示例中,属性索引指定所有名称为字符串的属性,它们的属性值必须是数值(number)。属性a的值为布尔值就报错了。
属性的数值索引
其实是指定数组的类型。
interface A {
[prop: number]: string;
}
const obj:A = ['a', 'b', 'c'];上面示例中,[prop: number]表示属性名的类型是数值,所以可以用数组对变量obj赋值。
同样的,一个接口中最多只能定义一个数值索引。数值索引会约束所有名称为数值的属性。
如果一个 interface 同时定义了字符串索引和数值索引,那么数值索引必须服从于字符串索引。因为在 JavaScript 中,数值属性名最终是自动转换成字符串属性名。
interface A {
[prop: string]: number;
[prop: number]: string; // 报错
}
interface B {
[prop: string]: number;
[prop: number]: number; // 正确
}上面示例中,数值索引的属性值类型与字符串索引不一致,就会报错。数值索引必须兼容字符串索引的类型声明。
函数
interface 也可以用来声明独立的函数。
interface Add {
(x:number, y:number): number;
}
const myAdd:Add = (x,y) => x + y;上面示例中,接口Add声明了一个函数类型。
interface 的继承
interface 可以继承其他类型,主要有下面几种情况。
interface 继承 interface
interface 可以使用extends关键字,继承其他 interface。
interface Shape {
name: string;
}
interface Circle extends Shape {
radius: number;
}上面示例中,Circle继承了Shape,所以圆其实有两个属性名称和半径。这时,Circle是子接口,Shape是父接口。
extends关键字会从继承的接口里面拷贝属性类型,这样就不必书写重复的属性。
interface 允许多重继承。
interface Style {
color: string;
}
interface Shape {
name: string;
}
interface Circle extends Style, Shape {
radius: number;
}上面示例中,Circle同时继承了Style和Shape,所以拥有三个属性color、name和radius。
多重接口继承,实际上相当于多个父接口的合并。
如果子接口与父接口存在同名属性,那么子接口的属性会覆盖父接口的属性。注意,子接口与父接口的同名属性必须是类型兼容的,不能有冲突,否则会报错。
interface Foo {
id: string;
}
interface Bar extends Foo {
id: number; // 报错
}上面示例中,Bar继承了Foo,但是两者的同名属性id的类型不兼容,导致报错。
多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错。
interface Foo {
id: string;
}
interface Bar {
id: number;
}
// 报错
interface Baz extends Foo, Bar {
type: string;
}上面示例中,Baz同时继承了Foo和Bar,但是后两者的同名属性id有类型冲突,导致报错。
interface 继承 type
interface 可以继承type命令定义的对象类型。
type Country = {
name: string;
capital: string;
}
interface CountryWithPop extends Country {
population: number;
}上面示例中,CountryWithPop继承了type命令定义的Country对象,并且新增了一个population属性。
注意,如果type命令定义的类型不是对象,interface 就无法继承。
接口合并
多个同名接口会合并成一个接口。
interface Box {
height: number;
width: number;
}
interface Box {
length: number;
}上面示例中,两个Box接口会合并成一个接口,同时有height、width和length三个属性。
这样的设计主要是为了兼容 JavaScript 的行为。JavaScript 开发者常常对全局对象或者外部库,添加自己的属性和方法。那么,只要使用 interface 给出这些自定义属性和方法的类型,就能自动跟原始的 interface 合并,使得扩展外部类型非常方便。
举例来说,Web 网页开发经常会对window对象和document对象添加自定义属性,但是 TypeScript 会报错,因为原始定义没有这些属性。解决方法就是把自定义属性写成 interface,合并进原始定义。
interface Document {
foo: string;
}
document.foo = 'hello';上面示例中,接口Document增加了一个自定义属性foo,从而就可以在document对象上使用自定义属性。
同名接口合并时,同一个属性如果有多个类型声明,彼此不能有类型冲突。
interface A {
a: number;
}
interface A {
a: string; // 报错
}上面示例中,接口A的属性a有两个类型声明,彼此是冲突的,导致报错。
同名接口合并时,如果同名方法有不同的类型声明,那么会发生函数重载。而且,后面的定义比前面的定义具有更高的优先级。
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()函数最后匹配的类型。
这个规则有一个例外。同名方法之中,如果有一个参数是字面量类型,字面量类型有更高的优先级。
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 命令。
它们的相似之处,首先表现在都能为对象类型起名。
type Country = {
name: string;
capital: string;
}
interface Country {
name: string;
capital: string;
}上面示例是type命令和interface命令,分别定义同一个类型。
interface 与 type 的区别有下面几点。
(1)type能够表示非对象类型,而interface只能表示对象类型(包括数组、函数等)。
(2)interface可以继承其他类型,type不支持继承。
继承的主要作用是添加属性,type定义的对象类型如果想要添加属性,只能使用&运算符,重新定义一个类型。
type Animal = {
name: string
}
type Bear = Animal & {
honey: boolean
}上面示例中,类型Bear在Animal的基础上添加了一个属性honey。
上例的&运算符,表示同时具备两个类型的特征,因此可以起到两个对象类型合并的作用。
作为比较,interface添加属性,采用的是继承的写法。
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
}继承时,type 和 interface 是可以换用的。interface 可以继承 type。
type Foo = { x: number; };
interface Bar extends Foo {
y: number;
}type 也可以继承 interface。
interface Foo {
x: number;
}
type Bar = Foo & { y: number; };(3)同名interface会自动合并,同名type则会报错。也就是说,TypeScript 不允许使用type多次定义同一个类型。
type A = { foo:number }; // 报错
type A = { bar:number }; // 报错上面示例中,type两次定义了类型A,导致两行都会报错。
作为比较,interface则会自动合并。
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灵活性比较高,便于扩充类型或自动合并,建议优先使用。
简单来说,就是如果你要定义的类型用interface和type都能实现,那么就用interface