联合与交叉类型
先看下面一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| interface Bird { fly: bollean; sing:() ={}; } interface Dog { fly: bollean; bark:() ={}; } function train(animal: Bird | Dog){ 这里可以使用fly方法,因为bird与dog中都有fly这个属性。 animal.fly(); //不可以直接使用。 animal.sing().... }
|
我们在这里定义了两个接口Bird与Dog,都有fly这个相同的属性,但是另外一个属性却不相同。如果我们需要让一个函数参数既可以接受Bird又可以接受Dog类型,那这时候我们就需要使用联合类型:使用’|’来枚举所有需要的类型。
有时候我们在传入某个对象后判断它的类型,比如在这个例子里,如果传入的animal是Dog类型,我们就调用bark方法,这个该怎么做呢?
在下面我们有四种方法。
有时候我们定义了两个类型,我们希望一个类型可以具有两个类型里面所有的属性和方法,这时就用到交叉类型,使用’&’:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| type landAnimal = { name: string canLiveOnLand: true } type waterAnimal = { name: string canLiveInWater: true } // 交叉类型:两栖动物 type amphibian = landAnimal & waterAnimal let toad: amphibian = { name: 'toad', canLiveOnLand: true, canLiveInWater: true, }
|
类型保护
有时候我们在传入某个对象后判断它的类型,比如在这个例子里,如果传入的animal是Dog类型,我们就调用bark方法,这个该怎么做呢?
在下面我们有四种方法。
使用as
我们可以通过传入对象里面有没有对应的方法来对对象的类型进行断言,进而使用其中的方法,具体的代码如下:
1 2 3 4 5 6 7
| function train ( animal: Bird | Dog){ if(animal.fly){ ( animal as Bird ).sing(); }else{ ( animal as Dog ).bark(); } }
|
使用as关键字相当于告诉编译器这个对象属于这个类,所以编译器也就可以找到这个类中是否有这个方法。
使用in
我们也可以使用in来判断这个对象里面有没有对应的属性或者方法:
1 2 3 4 5 6 7
| function train( animal: Bird | Dog){ if('sing' in animal){ animal.sing(); }else{ animal.bark(); } }
|
这里为什么else不用加判断呢,因为这里TS可以自动帮我们做类型推断,我们传入的类只有两种可能的类型,如果第一种里面没有sing这个方法,那就判断是否是第二个类型Dog。
使用typeof
假如现在有一个需求,写一个函数add,这个函数既可以接收string类型的参数,也可以接收number类型的参数,那么如果接收的是string类型的,我们就直接做字符串拼接,如果是数字类型返回相加结果。我们可以这里来写:
1 2 3 4 5 6 7
| function add( first: string | number, second: string | number){ if(typeof first ==='string' || typeof second === 'string'){ return `${first}${second}`; }else{ return first + second; } }
|
使用instanceof
我们先自定义一个类NumberObj:
1 2 3
| class NumberObj { count: number; }
|
对于传入的如果是一个对象类型,我们还可以使用instanceof来进行类型保护,假如我们现在当两个参数都为我们自定义的NumberObj的时候我们就返回他们的count属性相加,如果是其他对象类型就返回0。可以这样:
1 2 3 4 5 6 7
| function add( first: obj | NumberObj, second: obj | NumberObj){ if(first instanceof NumberObj && second instanceof NumberObj){ return first.count + second.count; }else{ return 0; } }
|
这里instanceof 就是判断该对象是不是该类的一个实例。
枚举类型
假如我们有这样一个需求,根据输入的状态码返回对应的状态说明:
1 2 3 4 5 6 7 8 9
| function getRes( staus ){ if(staus ===0){ return 'offline'; }elseif(staus ===1){ return 'online'; }elseif(staus === 2){ return 'error'; } }
|
这样我们的代码可读性就会非常不好,那么有什么办法可以改进呢?我们可以创建一个对象,通过属性的方式去访问里面的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const status = { OFFLINE: 0, ONLINE: 1, DELETED: 2, }
function getRes( staus ){ if(staus === status.OFFLINE){ return 'offline'; }elseif(staus ===status.ONLINE){ return 'online'; }elseif(staus === staus.DELETED){ return 'error'; } }
|
上面是JS的写法,在TS中可以使用枚举类去更加灵活的实现。枚举类型就是通过在一个集合里列出所有需要的类型,使得它比普通的类型范围更加精确。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| enum Status { OFFLINE ONLINE DELETED }
function getRes( staus ){ if(staus === status.OFFLINE){ return 'offline'; }elseif(staus ===status.ONLINE){ return 'online'; }elseif(staus === staus.DELETED){ return 'error'; } } const result = getRes(0) //这里打印出'offline',因为默认枚举元素的值从0开始递增。 console.log(result);
|
泛型
泛型允许我们在定义属性和方法的时候不指定类型,而在对变量赋值的时候指定,这样的好处显而易见,有时候我们需要一个函数去应付不同类型的参数,传统的方法只能一个个的指定,但是如果参数类型不确定,或者以后的维护过程会增加类型,那么使用泛型开发会使得代码更加简洁优雅。
泛型函数
比如我们有这样一个需求:写一个函数,返回两个参数的字符串拼接。我们可以使用尖括号’<>’来说明一个函数或者一个类是泛型,尖括号里面的值是自定义的泛型名词,约定中一般使用用T。下面就是该函数的定义和使用:
1 2 3 4 5
| function join<T>( first: T, second: T ){ return `${first}${second}`; } //使用的时候可以指定类型 join<number>( 1,1 );
|
泛型还还可以作为数组的元素,如下所示:
1 2 3 4 5
| function map<T>(paras: Array<T>){ return paras; } //使用的时候可以指定类型 join<string>(['123','234']);
|
还可以定义多个泛型,比如修改上面的函数如下所示:
1 2 3 4 5
| function join<T,P>( first: T, second: P){ return `${first}${second}`; } //使用的时候如果不指定类型,编译器会根据传入的类型进行推断。 join( 1,'1' );
|
也可以不用显示的声明传入变量的类型,TS编译器会智能的对类型进行推断。
这里可能会有一些疑问,为什么不可以直接用any类型去限定一个变量呢,因为泛型虽然不限制我们传入变量的类型,但是如果函数参数有多个变量,我们想要限定他们的类型相同,比如都是字符串或者数组,或者返回值也是入参的类型,这个时候就只能用泛型来限定。
泛型类
假设现在有这样一个类DataManager,他接收一个数组,对外提供一个方法可以根据索引返回对应索引处数组的值:我们现在希望这个数组既可以是字符串类型的,也可以是数字类型的,我们可以用联合类型这样声明:
1 2 3 4 5 6 7 8 9
| class DataManager { constructor(private data: string[] | number[] ){}; getItem(index: number):string | number { return this.data[index]; } }
const data = new DataManager([1,2]); data.getItem(0);
|
这样就可以实现这个功能了,但是假如又有了新的类型,修改起来就比较麻烦,用泛型定义会使得我们可以去掉很长的类型定义:
1 2 3 4 5 6 7 8 9
| class DataManager<T> { constructor(private data: T[] ){}; getItem(index: number):T { return this.data[index]; } }
const data = new DataManager([1,2]); data.getItem(0);
|
假如我们又有一个新的要求,要求每个T类型的数据里面有一个name属性,我们这时就要定义一个数据结构含有这个name属性,并且让T类型继承我们这个自定义的属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| interface Item{ name: string; } class DataManager<T extends Item> { constructor(private data: T[] ){}; getItem(index: number):string { return this.data[index].name; } } //我们new对象的时候需要满足这个类型的继承约束,即含有name属性 const data = new DataManager([ { name: "wm"; ... }]
|