TypeScript入门教程
介绍
TypeScript是静态类型
的弱类型语言,在编译阶段就能确定每个变量的类型,编译器可以进行类型检查并且会标注出语法错误。
弱类型:因为完全兼容Javascript,所以同样也是弱类型,即可以进行隐式类型转换,如:
1 | console.log(1 + '1'); |
但我们可以通过声明变量的类型,将Ts作为一个强类型语言来使用。
增强了IED的功能,提供代码补全等能力。
数据类型
变量声明方式
1 | let num: number = 1; |
注意,如果使用Number()去创建一个变量,这个变量并不是number类型,而是Number类型。前者是基本数据类型,而后者是一个构造函数对象。
空值
可以指定一个函数的返回值为空值(void)
1 | function test(): void { |
可以将指定变量类型为void,此时只能将这个变量赋值为undefined和null
1 |
|
等价于
1 | let r:any; |
any和unkown的区别
unknown类型和any类型类似。与any类型不同的是。unknown类型可以接受任意类型赋值,但是unknown类型赋值给其他类型前,必须被断言
类型推论
上面已经说过,声明对象时,如果没有指定其类型,就相当于定义了一个任意值类型,这其实就是类型推断的一个机制。
Ts会在未指定变量类型的时候,给变量一个推断的类型,如:
1 | let n = 123; |
相当于
1 | let n: number = 123 |
联合类型
联合类型表示取值可以为多种类型中的一种,类型之间用|
分隔,比如:
1 | let n: number | string = 1 |
访问联合属性的属性和方法时,如果不确定变量到底是联合类型中的哪个类型,我们就只能访问到此联合类型所有类型的属性和方法。
1 | function getLength(something: string | number): number { |
如上例,因为length
不是String
和Number
的共有方法,所以会报错,而:
1 | function getString(something: string | number): string { |
是可以访问的,因为toString
是String
和Number
的共有方法。
联合变量在被赋值时会执行类型推论。
类型别名
使用type
关键字创建类型别名。常用于联合类型。
1 | type Name = string; |
字符串字面量类型
同样可以用type
来定义,约束取值只能是某几个字符串中的一个。
1 | type EventNames = 'click' | 'scroll' | 'mousemove'; |
接口
接口(interface)是对象的类型
,是对行为的抽象。而具体的就要靠类(class)去实现(implement)。
Ts中的接口除了可以对类的一部分行为进行抽象,还可以用来对对象的形状进行描述。
1 | interface Person { |
接口一般首字母大写 ,推荐使用I开头。
可选属性
对象的属性必须与接口一致,既不能多也不能少,所以如果需要指定可选属性
,可以在接口的属性名后面加上?
,那么这样的赋值就是允许的:
1 | interface Person { |
扩展:TS中?的其他用法
可选链(.?):
传入的data参数可能为null,undefined,通常我们的写法是直接上if判断,然后再取data中的属性,但是有了问号点(?.)写法就简单很多了,看下面例子:
1 | //1.data可能为null,undefined , row也可能为null,undefined |
等价于
1 | //1.data可能为null,undefined , row也可能为null,undefined |
从上面写法可以看出来问号点(?.)的写法其实等价于例2的if判断、三元运算符(let a = b == null ? null : b.a)
空值合并操作符(??)
我们想判断一个值存在,一般需要考虑0的情况,比如:
1 | let a = 0; |
使用??的简化写法:
1 | let b; |
这样写的含义是:当a为非空值(非undefined、null)时b=a,否则b=c。
空值赋值运算符(??=)
1 | let b = 'hello'; |
当??=左侧的值为null、undefined的时候,才会将右侧变量的值赋值给左侧变量。
!的用法
!不可为空的成员
1 | // 这里的?表示这个name属性有可能不存在 |
!.在变量名后添加!,可以断言排除undefined和null类型。
1 | let a: string | null | undefined |
!:,待会分配这个变量。
任意属性
也叫做可索引类型
。
如果希望一个接口允许有任意属性
,可以加上[propName:类型]:any。其中propName类似形参,名字可以任意指定。
1 | interface Person { |
先看官方文档解释:可索引类型具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。
则[propName: string] : any就是描述了对象索引的类型为string,索引返回值类型为any。
再通俗一点来讲,[propName: string] : any 就表示键为string类型,值为any类型的属性。
多个任意属性存在的情况
索引签名可以使用字符串和数字两种类型的索引。
不同类型的索引可以同时声明
。但是当两种类型索引同时存在时,number类型索引的返回值类型必须是string类型索引的返回值类型的子类型
。
官方解释:当使用
number
来索引时,JavaScript会将它转换成string
然后再去索引对象。 也就是说用100
(一个number
)去索引等同于使用"100"
(一个string
)去索引,因此两者需要保持一致。
相同类型的索引不能同时声明
。如果接口中给有多个类型属性,则可以在任意属性中使用联合属性:
1 | interface Person { |
存在任意属性和其他属性的情况
注意:一旦定义了任意类型属性,那么确定属性和可选属性的类型都必须是它的类型的子集。
1 | interface Person { |
如上,有一个任意类属性规定类型为string,可选属性age类型为number,不是string的子类,所以会报错。
当索引的类型是number时,则有特殊的情况:其他属性如果索引类型为string,则此索引返回值类型不会受前者索引返回值类型的影响,如:
1 | type Arg = { |
虽然有任意属性和其他属性同时存在,但length的索引类型为string,所以不会受index的影响,此声明是合理的。
而:
1 | type MyArray = { |
因为0是number类型的索引,所以会受到index的影响。
一个接口是可以同时定义两种任意属性的。但number类型的属性值类型必须是string类型的属性值类型的子集。如:
1 | interface C { |
因为index指定的值类型是string,不是prop的值类型number的子集,所以会报错。而
1 | interface C { |
则是成立的。因为Function是object的子集。
只读属性
属性前面使用readonly关键字,一旦赋值,不可以被改写。
示例:
1 | interface Person { |
报错信息有两处,第一处是在对 tom
进行赋值的时候,没有给 id
赋值。
第二处是在给 tom.id
赋值的时候,由于它是只读属性,所以报错了。
数组
数组有多种表示方法。
变量名:类型[]表示
1 | let arr: number[] = [1,2,3,4,5]; |
数组泛型表示
1 | let arr: Array<number> = [1,2,3,4,5]; |
接口表示
1 | interface numArray { |
约束了索引类型为number类型时,对应的值类型也必须是number类型。
虽然这种方式可以用来描述数组,但是一般使用前两种方式,声明比较简单。但是可以用来表示类数组。
类数组
类数组不是数组类型,比如arguments,不能用第一、二中方式来表示。
1 | function sum() { |
应该使用接口方式来表示:
1 | function sum() { |
类数组有自己的接口定义,比如IArguments等。
any在数组中的应用
any允许数组中出现任意类型。
let arr: any[] = [1,’2’,{}]
元组
数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。
示例
1 | let tom: [string, number] = ['Tom', 25]; |
当赋值或访问一个已知索引的元素时,会得到正确的类型:
1 | let tom: [string, number]; |
也可以只赋值其中一项:
1 | let tom: [string, number]; |
但是当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项。
1 | let tom: [string, number]; |
1 | let tom: [string, number]; |
越界的元素
当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型:
1 | let tom: [string, number]; |
枚举
枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。
枚举使用 enum
关键字来定义:
1 | enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat}; |
枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:
1 | enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat}; |
事实上,上面的例子会被编译为:
1 | var Days; |
手动赋值
我们也可以给枚举项手动赋值:
1 | enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat}; |
上面的例子中,未手动赋值的枚举项会接着上一个枚举项递增
。
如果未手动赋值的枚举项与手动赋值的重复了,后面的会覆盖前面的:
1 | enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat}; |
相当于编译成
1 | var Days; |
手动赋值的枚举项可以不是数字,此时需要使用类型断言来让 tsc 无视类型检查 (编译出的 js 仍然是可用的):
1 | enum Days {Sun = 7, Mon, Tue, Wed, Thu, Fri, Sat = <any>"S"}; |
当然,手动赋值的枚举项也可以为小数或负数,此时后续未手动赋值的项的递增步长仍为 1
:
1 | enum Days {Sun = 7, Mon = 1.5, Tue, Wed, Thu, Fri, Sat}; |
常数项和计算所得项
枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。
前面我们所举的例子都是常数项,一个典型的计算所得项的例子:
1 | enum Color {Red, Green, Blue = "blue".length}; |
上面的例子中,"blue".length
就是一个计算所得项。
上面的例子不会报错,但是如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错:
1 | enum Color {Red = "red".length, Green, Blue}; |
所以计算所得项一定要写在未赋值项的最后
注意:使用了+
, -
, ~
一元运算符,+
, -
, *
, /
, %
, <<
, >>
, >>>
, &
, |
, ^
二元运算符,也算是常数表达式。若常数枚举表达式求值后为 NaN 或 Infinity,则会在编译阶段报错。
常数枚举
常数枚举是使用 const enum
定义的枚举类型:
1 | const enum Directions { |
常数枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员。
上例的编译结果是:
1 | var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */]; |
假如包含了计算成员,则会在编译阶段报错:
1 | const enum Color {Red, Green, Blue = "blue".length}; |
函数
声明方式
对比js中函数有两种定义的方式:函数声明
和函数表达式
。
1 | // 函数声明(Function Declaration) |
Typescript的函数声明法
1 | function sum(x:number,y:number):number { |
对输入输入的类型进行限制。调用函数时,参数的数量必须与形参匹配,否则会报错。
Typescript的函数表达式法
如果要我们现在写一个对函数表达式(Function Expression)的定义,可能会写成这样:
1 | let mySum = function (x: number, y: number): number { |
这是可以通过编译的,不过事实上,上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的 mySum
,是通过赋值操作进行类型推论而推断出来的。如果需要我们手动给 mySum
添加类型,则应该是这样:
1 | let mySum: (x: number, y: number) => number = function (x: number, y: number): number { |
注意区分ES6的箭头函数。在 TypeScript 的类型定义中,=>
用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
利用接口定义函数形状
1 | interface SearchFunc { |
保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。
可选参数
与接口中的可选属性类似,我们用 ?
表示可选的参数:
1 | function buildName(firstName: string, lastName?: string) { |
需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了:
1 | function buildName(firstName?: string, lastName: string) { |
参数默认值
在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数:
1 | function buildName(firstName: string, lastName: string = 'Cat') { |
此时就不受「可选参数必须接在必需参数后面」的限制了:
1 | function buildName(firstName: string = 'Tom', lastName: string) { |
剩余参数
类似于ES6 中,可以使用 ...rest
的方式获取函数中的剩余参数(ret参数):
1 | function push(array, ...items) { |
事实上,items
是一个数组。在TS中我们可以用数组的类型来定义它:
1 | function push(array: any[], ...items: any[]) { |
注意,rest 参数只能是最后一个参数
。
重载
重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。
比如我们要实现字符串或者数字的反转,联合类型的实现方式是:
1 | function reverse(x: number | string): number | string | void { |
但是这样写无法将返回结果和参数进行对应,即输入字符串应返回字符串,输入数字应返回数字。
这时就可以用重载去定义多个函数类型。
1 | function reverse(x: number): number; |
原理是,TS会优先从前面的函数开始匹配,即前面应该写更精确的定义。
类型断言
类型断言可以用来手动指定一个值的类型。
写法是
1 | 值 as 类型 |
或
1 | <类型>值 |
尽量使用前者as的语法,第二种可能遇到不支持的情况。
类型断言的用途
将一个联合类型断言为其中一个类型
因为前面当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法。
1 | interface Cat { |
而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法。
1 | interface Cat { |
上面的例子中,获取 animal.swim
的时候会报错。
此时可以使用类型断言,将 animal
断言成 Fish
:
1 | interface Cat { |
但是需要注意,类型断言不能滥用。编译器会信任我们的断言,有时候虽然编译时不会报错,但是可能会运行时报错。
将一个父类断言为更加具体的子类
当类之间有继承关系时,类型断言也是很常见的:
1 | class ApiError extends Error { |
上面的例子中,我们声明了函数 isApiError
,它用来判断传入的参数是不是 ApiError
类型,为了实现这样一个函数,它的参数的类型肯定得是比较抽象的父类 Error
,这样的话这个函数就能接受 Error
或它的子类作为参数了。
但是由于父类 Error
中没有 code
属性,故直接获取 error.code
会报错,需要使用类型断言获取 (error as ApiError).code
。
大家可能会注意到,在这个例子中有一个更合适的方式来判断是不是 ApiError
,那就是使用 instanceof
:
1 | class ApiError extends Error { |
上面的例子中,确实使用 instanceof
更加合适,因为 ApiError
是一个 JavaScript 的类,能够通过 instanceof
来判断 error
是否是它的实例。
但是有的情况下 ApiError
和 HttpError
不是一个真正的类,而只是一个 TypeScript 的接口(interface
),接口是一个类型,不是一个真正的值,它在编译结果中会被删除,当然就无法使用 instanceof
来做运行时判断了:
1 | interface ApiError extends Error { |
此时就只能用类型断言,通过判断是否存在 code
属性,来判断传入的参数是不是 ApiError
了:
1 | interface ApiError extends Error { |
将任何一个类型断言为 any
1 | const foo: number = 1; |
上面的例子中,数字类型的变量 foo
上是没有 length
属性的,故 TypeScript 给出了相应的错误提示。
这种错误提示显然是非常有用的。
但有的时候,我们非常确定这段代码不会出错,比如下面这个例子:
1 | window.foo = 1; |
上面的例子中,我们需要将 window
上添加一个属性 foo
,但 TypeScript 编译时会报错,提示我们 window
上不存在 foo
属性。
此时我们可以使用 as any
临时将 window
断言为 any
类型:
1 | (window as any).foo = 1; |
在 any
类型的变量上,访问任何属性都是允许的。
一方面不能滥用 as any
,另一方面也不要完全否定它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡(这也是 TypeScript 的设计理念之一),才能发挥出 TypeScript 最大的价值。
将 any 断言为一个具体的类型
1 | function getCacheData(key: string): any { |
上面的例子中,我们调用完 getCacheData
之后,立即将它断言为 Cat
类型。这样的话明确了 tom
的类型,后续对 tom
的访问时就有了代码补全,提高了代码的可维护性。
类型断言的限制
是不是任何一个类型都可以被断言为任何另一个类型呢?
并非如此。
具体来说,若 A
兼容 B
,那么 A
能够被断言为 B
,B
也能被断言为 A
。
1 | interface Animal { |
在上面的例子中,Cat
包含了 Animal
中的所有属性,除此之外,它还有一个额外的方法 run
。TypeScript 并不关心 Cat
和 Animal
之间定义时是什么关系,而只会看它们最终的结构有什么关系——所以它与 Cat extends Animal
是等价的:
1 | interface Animal { |
我们把它换成 TypeScript 中更专业的说法,即:Animal
兼容 Cat
。
当 Animal
兼容 Cat
时,它们就可以互相进行类型断言了:
1 | interface Animal { |
父类可以断言为子类,子类也可以断言为父类。
双重断言
既然任何类型都可以被断言为any,any也可以被断言为任何类型,那么我们可以使用双重断言 as any as Foo
来将任何一个类型断言为任何另一个类型。
1 | interface Cat { |
若你使用了这种双重断言,那么十有八九是非常错误的,它很可能会导致运行时错误。
除非迫不得已,千万别用双重断言。
类型断言与类型转换
类型断言只会影响 TypeScript 编译时
的类型,类型断言语句在编译结果中会被删除:
1 | function toBoolean(something: any): boolean { |
在上面的例子中,将 something
断言为 boolean
虽然可以通过编译,但是并没有什么用,代码在编译后会变成:
1 | function toBoolean(something) { |
所以类型断言不是类型转换,它不会真的影响到变量的类型。若要进行类型转换,需要直接调用类型转换的方法。
类型断言与类型声明
在这个例子中:
1 | function getCacheData(key: string): any { |
我们使用 as Cat
将 any
类型断言为了 Cat
类型。
但实际上还有其他方式可以解决这个问题:
1 | function getCacheData(key: string): any { |
上面的例子中,我们通过类型声明的方式,将 tom
声明为 Cat
,然后再将 any
类型的 getCacheData('tom')
赋值给 Cat
类型的 tom
。
这和类型断言是非常相似的,而且产生的结果也几乎是一样的——tom
在接下来的代码中都变成了 Cat
类型。
它们的区别,可以通过这个例子来理解:
1 | interface Animal { |
在上面的例子中,由于 Animal
兼容 Cat
,故可以将 animal
断言为 Cat
赋值给 tom
。
但是若直接声明 tom
为 Cat
类型:
1 | interface Animal { |
则会报错,不允许将 animal
赋值为 Cat
类型的 tom
。
这是因为,子类实例可以赋值给类型为父类的变量,而父类实例不可以赋值给类型为子类的变量。
深入的讲,它们的核心区别就在于:
animal
断言为Cat
,只需要满足Animal
兼容Cat
或Cat
兼容Animal
即可animal
赋值给tom
,需要满足Cat
兼容Animal
才行(即tom的类型是animal的父类),上例并不满足。
而any类型是双重兼容任意类型,所以此时的类型断言与类型声明等价。
类型断言与泛型
通过给 getCacheData
函数添加了一个泛型 <T>
,我们可以更加规范的实现对 getCacheData
返回值的约束,这也同时去除掉了代码中的 any
,是最优的一个解决方案。
类
Js中类的用法
属性和方法
使用 class
定义类,使用 constructor
定义构造函数。
通过 new
生成新实例的时候,会自动调用构造函数。
1 | class Animal { |
类的继承
使用 extends
关键字实现继承,子类中使用 super
关键字来调用父类的构造函数和方法。
1 | class Cat extends Animal { |
存取器
使用 getter 和 setter 可以改变属性的赋值和读取行为:
1 | class Animal { |
静态方法
使用 static
修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用:
1 | class Animal { |
ES7 中有一些关于类的提案,TypeScript 也实现了它们,这里做一个简单的介绍。
实例属性
ES6 中实例的属性只能通过构造函数中的 this.xxx
来定义,ES7 提案中可以直接在类里面定义:
1 | class Animal { |
静态属性
ES7 提案中,可以使用 static
定义一个静态属性:
1 | class Animal { |
TS中类的用法
public private 和 protected
TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public
、private
和 protected
。
public
修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是public
的private
修饰的属性或方法是私有的,不能在声明它的类的外部访问protected
修饰的属性或方法是受保护的,它和private
类似,区别是它在子类中也是允许被访问的
当构造函数修饰为 private
时,该类不允许被继承或者实例化:
1 | class Animal { |
当构造函数修饰为 protected
时,该类只允许被继承:
1 | class Animal { |
修饰符可以当作参数用在构造函数中,等同于类中定义该属性同时给该属性赋值,使代码更简洁。
1 | class Animal { |
readonly
只读属性关键字,只允许出现在属性声明或索引签名或构造函数中。
1 | class Animal { |
注意如果 readonly
和其他访问修饰符同时存在的话,需要写在其后面。
1 | class Animal { |
抽象类
abstract
用于定义抽象类和其中的抽象方法。
什么是抽象类?
首先,抽象类是不允许被实例化的:
1 | abstract class Animal { |
上面的例子中,我们定义了一个抽象类 Animal
,并且定义了一个抽象方法 sayHi
。在实例化抽象类的时候报错了。
其次,抽象类中的抽象方法必须被子类实现:
1 | abstract class Animal { |
上面的例子中,我们定义了一个类 Cat
继承了抽象类 Animal
,但是没有实现抽象方法 sayHi
,所以编译报错了。
下面是一个正确使用抽象类的例子:
1 | abstract class Animal { |
子类继承抽象类,同时需要实现抽象方法。
需要注意的是,即使是抽象方法,TypeScript 的编译结果中,仍然会存在这个类
类的类型
给类加上 TypeScript 的类型很简单,与接口类似:
1 | class Animal { |
类与接口
之前学习过,接口(Interfaces)可以用于对「对象的形状(Shape)」进行描述。
这一章主要介绍接口的另一个用途,对类的一部分行为进行抽象。
类实现接口
实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements
关键字来实现。这个特性大大提高了面向对象的灵活性。
举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它:
1 | interface Alarm { |
一个类可以实现多个接口:
1 | interface Alarm { |
上例中,Car
实现了 Alarm
和 Light
接口,既能报警,也能开关车灯。
接口继承接口
接口与接口之间可以是继承关系:
1 | interface Alarm { |
这很好理解,LightableAlarm
继承了 Alarm
,除了拥有 alert
方法之外,还拥有两个新方法 lightOn
和 lightOff
。
接口继承类
常见的面向对象语言中,接口是不能继承类的,但是在 TypeScript 中却是可以的:
1 | class Point { |
实际上,当我们在声明 class Point
时,除了会创建一个名为 Point
的类之外,同时也创建了一个名为 Point
的类型(实例的类型)。
所以我们既可以将 Point
当做一个类来用(使用 new Point
创建它的实例):
1 | class Point { |
也可以将 Point
当做一个类型来用(使用 : Point
表示参数的类型):
1 | class Point { |
这个例子实际上可以等价于:
1 | class Point { |
上例中我们新声明的 PointInstanceType
类型,与声明 class Point
时创建的 Point
类型是等价的。
值得注意的是,PointInstanceType
相比于 Point
,缺少了 constructor
方法,这是因为声明 Point
类时创建的 Point
类型是不包含构造函数的。另外,除了构造函数是不包含的,静态属性或静态方法也是不包含的(实例的类型当然不应该包括构造函数、静态属性或静态方法)。
也就是说,声明 Point
类时创建的 Point
类型只包含其中的实例属性和实例方法
泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
首先,我们来实现一个函数 createArray
,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:
1 | function createArray(length: number, value: any): Array<any> { |
上例中,我们使用了之前提到过的数组泛型来定义返回值的类型。
这段代码编译不会报错,但是一个显而易见的缺陷是,它并没有准确的定义返回值的类型:
Array<any>
允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该是输入的 value
的类型。
这时候,泛型就派上用场了:
1 | function createArray<T>(length: number, value: T): Array<T> { |
上例中,我们在函数名后添加了 <T>
,其中 T
用来指代任意输入的类型,在后面的输入 value: T
和输出 Array<T>
中即可使用了。
接着在调用的时候,可以指定它具体的类型为 string
。当然,也可以不手动指定,而让类型推论自动推算出来:
1 | function createArray<T>(length: number, value: T): Array<T> { |
多个类型参数
定义泛型的时候,可以一次定义多个类型参数:
1 | function swap<T, U>(tuple: [T, U]): [U, T] { |
上例中,我们定义了一个 swap
函数,用来交换输入的元组。
泛型约束
在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:
1 | function loggingIdentity<T>(arg: T): T { |
上例中,泛型 T
不一定包含属性 length
,所以编译的时候报错了。
这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length
属性的变量。这就是泛型约束:
1 | interface Lengthwise { |
上例中,我们使用了 extends
约束了泛型 T
必须符合接口 Lengthwise
的形状,也就是必须包含 length
属性。
此时如果调用 loggingIdentity
的时候,传入的 arg
不包含 length
,那么在编译阶段就会报错。
多个类型参数之间也可以互相约束:
1 | function copyFields<T extends U, U>(target: T, source: U): T { |
上例中,我们使用了两个类型参数,其中要求 T
继承 U
,这样就保证了 U
上不会出现 T
中不存在的字段。
泛型接口
之前学习过,可以使用接口的方式来定义一个函数需要符合的形状:
1 | interface SearchFunc { |
当然也可以使用含有泛型的接口来定义函数的形状:
1 | interface CreateArrayFunc { |
进一步,我们可以把泛型参数提前到接口名上:
1 | interface CreateArrayFunc<T> { |
注意,此时在使用泛型接口的时候,需要定义泛型的类型。
泛型类
与泛型接口类似,泛型也可以用于类的类型定义中:
1 | class GenericNumber<T> { |
泛型参数的默认类型
在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。
1 | function createArray<T = string>(length: number, value: T): Array<T> { |
声明合并
如果定义了两个相同名字的函数、接口或类,那么它们会合并成一个类型:
函数的合并
之前学习过,我们可以使用重载定义多个函数类型:
1 | function reverse(x: number): number; |
接口的合并
接口中的属性在合并时会简单的合并到一个接口中:
1 | interface Alarm { |
相当于:
1 | interface Alarm { |
注意,合并的属性的类型必须是唯一的:
1 | interface Alarm { |
接口中方法的合并,与函数的合并一样:
1 | interface Alarm { |
相当于:
1 | interface Alarm { |
类的合并
类的合并与接口的合并规则一致。
文档参考
TypeScript入门教程:学TypeScript时跟着这个教程学到了最后,自己又再次整理了一遍。感谢原作者。
TypeScript官方手册中文版:感谢 @zhongsp大佬对官方手册的翻译。