TypeScript真香系列-高级类型
前言
TypeScript真香系列的内容将参考中文文档,但是文中的例子基本不会和文档中的例子重复,对于一些地方也会深入研究。另外,文中一些例子的结果都是在代码没有错误后编译为JavaScript得到的。如果想实际看看TypeScript编译为JavaScript的代码,可以访问TypeScript的在线编译地址,动手操作,印象更加深刻。
交叉类型
交叉类型是将多个类型合并为一个类型,相当于一种并的操作。
interface IDog { name: string, age: number, } interface ICat { name: string, color: string } let animal: IDog & ICat; animal = { name: "哈士奇", age: 1, color: "white", } animal.name; // "哈士奇" animal.age; // 1
上面animal中的属性一个都不能少,如果少了属性的话,就会出现下面的错误:
i
nterface IDog { name: string, age: number, } interface ICat { name: string, color: string } let animal: IDog & ICat; animal = { //错误,color属性在ICat中是必须的 name: "哈士奇", age: 1, // color: "white", }
联合类型
联合类型可以说是和交叉类型相反,声明的类型不确定,可以是多个类型中的一个或几个。
let a: number | string; a = 1; a = "s"; a = false; // 错误,类型false不能分配给类型 number | string
看一个和交叉类型相对应的例子:
interface IDog { name: string, age: number, } interface ICat { name: string, color: string } let animal: IDog | ICat; // 这里我们把&改成了| animal = { //没有报错 name: "哈士奇", age: 1, // color: "white", } animal.name; animal.age;
再看一个例子:
interface IDog { name: string, age: number, } interface ICat { name: string, color: string } let animal: IDog | ICat; animal = { name: "哈士奇", age: 1, color: "white", } animal.name; animal.age; //错误,age不存在于ICat. age不存在于IDog | ICat
我们可以看见上面的例子出现了错误,这是因为TypeScript编译器age不知道是IDog还是ICat,所以只能访问公共的name属性。如果我们想要访问这个属性的话,该怎么办?我们可以使用类型断言:
interface IDog { name: string, age: number, } interface ICat { name: string, color: string } let animal: IDog | ICat; animal = { name: "哈士奇", age: 1, color: "white", } animal.name; (<IDog>animal).age; // 1
这下就能访问age属性了。
类型保护
有时候我们会遇到类似于下面这种场景:
interface IDog { name: string, age: number, } interface ICat { name: string, color: string } function animal(arg: IDog | ICat): any { if (arg.color) { //错误 return arg.color //错误 } }
但是上面的代码会出现错误。如果想要上面这段代码正常工作,可以和联合类型中的例子一样,使用类型断言:
interface IDog { name: string, age: number, } interface ICat { name: string, color: string } function animal(arg: IDog | ICat):any { if ((<ICat>arg).color) { return (<ICat>arg).color; } }
除了类型断言,我们还可以利用类型保护来进行判断,常用的类型保护有三种:typeof类型保护,instanceof类型保护和自定义类型保护。
typeof类型保护
function animal(arg: number | string): any { if (typeof arg === "string") { return arg + "猫"; } }
typeof类型保护只有两种形式能被识别: typeof v === "typename"
和 typeof v !== "typename"
。”typename”必须是 “number”, “string”, “boolean”或 “symbol”。
instanceof类型保护
class Dog { name: string; age: number; constructor() { }; } class Cat { name: string; color: string; constructor() { }; } let animal: Dog | Cat = new Dog(); if (animal instanceof Dog) { animal.name = "dog"; animal.age = 6; } if (animal instanceof Cat) { animal.name = "cat"; animal.color = "white"; } console.log(animal); //Dog {name: "dog", age: 6}
instanceof的右侧要求是一个构造函数,TypeScript将细化为:
- 此构造函数的 prototype属性的类型,如果它的类型不为 any的话;
- 构造签名所返回的类型的联合。
自定义类型保护
对于一些复杂的情况,我们可以自定义来进行类型保护:
interface IDog { name: string, age: number, } interface ICat { name: string, color: string } let animal: IDog | ICat; animal = { name: "哈士奇", age: 6, } function isDog(arg: IDog | ICat): arg is IDog { return arg !== undefined; } if (isDog(animal)) { console.log(animal.age); //6 }
类型别名
类型别名可以给类型取一个别名。类型别名和接口类似,但又有不同。
type Name = number; type Types = number | string; type NAndT = Name & Types; type MyFunc = () => number; function animal(arg: Types) { return arg; } animal("哈士奇"); //"哈士奇"
类型别名可以作用于原始类型、联合类型、泛型等等。
type Dog<T> = { value: T }; function dog(arg: Dog<string>) { return arg; } dog({ value: "哈士奇" }); //{value: "哈士奇"} dog({ value: 1}); //错误,类型number不能分配给string dog("哈士奇"); //错误,参数“哈士奇”不能分配给类型 Dog<string>
接口和类型别名的区别
区别一:接口可以创建新的名字,而且可以在其它任何地方使用;类型别名不创建新的名字,而是起一个别名。
区别二:类型别名可以进行联合,交叉等操作。
区别三:接口可以被extends和implements以及声明合并等,而类型别名不可以。
这里介绍一下声明合并:
“声明合并”是指编译器将针对同一个名字的两个独立声明合并为单一声明。 合并后的声明同时拥有原先两个声明的特性。
任何数量的声明都可被合并;不局限于两个声明。
举个例子:
interface IDog { name: string; setName(arg:string): string; } interface IDog { age: number; } interface IDog { color: string; } let dog: IDog; dog = { color: "black", age: 6, name: "哈士奇", setName: (arg) => { return arg } }
合并之后:
interface IDog { color: string; age: number; name: string; setName(arg:string): string; }
我们可以看出,后面的接口在合并后出现在了靠前的位置。
字符串字面量类型和数字字面量类型
字符串字面量允许我们指定字符串为必须的固定值。
type Dog = "哈士奇" | "泰迪" | "中华田园犬" | "萨摩耶"; function dog(arg: Dog):any { switch (arg) { case "哈士奇": return "傻狗"; case "泰迪": return "精力旺盛"; case "中华田园犬": return "忠诚"; case "萨摩耶": return "微笑天使"; } } dog("哈士奇"); //"傻狗" dog("柯基"); //错误,参数"柯基"不能分配给类型Dog
数字字面量同理。
type Num = 1 | 2 | 3 | 4 | 5 | 6 | 7; function week(arg: Num):any { switch (arg) { case 1: return "星期一"; case 2: return "星期二"; case 3: return "星期三"; case 4: return "星期四"; case 5: return "星期五"; case 6: return "星期六"; case 7: return "星期日"; } } week(6); //"星期六" week(8); //错误
可辨识联合
我们可以合并单例类型、联合类型、类型保护和类型别名来创建一个叫做可辨识联合的高级模式。它具有三个要素:
- 有普通的单例类型属性— 可辨识的特征。
- 一个类型别名包含了那些类型的联合— 联合。
- 此属性上的类型保护。
可以看看下面这个例子就能理解了:
//我们首先声明了将要联合的接口,目前各个接口之间是没有联系的, //只是都有一个kind的属性(可以称为可辨识特征或标签)但是有不同的字符串字面量类型 interface IColor { kind: "color"; value: string; } interface ISize { kind: "size"; height: number; width: number; } //然后我们利用类型别名和联合类型把两个接口联合到一起 type MyType = IColor | ISize; //最后使用可辨识联合 function types(arg: MyType) :any{ switch (arg.kind) { case "color": return arg.value; case "size": return arg.height * arg.width; } } types({ kind: "color", value: "blue" }); //"blue" types({ kind: "size", height: 10, width: 20 }); //200
索引类型
使用索引类型,编译器就能够检查使用动态属性名的代码。我们首先要知道两个操作符。
- 索引类型的查询操作符
keyof T
,意为:对于任何类型T
,keyof T
的结果为T
上已知公共属性的联合; - 索引访问操作符
T[K]
。
interface IDog{ a: string, b: number, c: boolean, } let dog: keyof IDog; //let dog: "a" | "b" | "c" let arg: IDog["a"]; //let arg: string
再看一个较为复杂的例子:
interface IDog{ name: string, age: number, value: string, } let dog: IDog; dog = { name: "二哈", age: 6, value: "奥里给" } function myDog<T, K extends keyof T>(x: T, args: K[]): T[K][] { return args.map(i => x[i]); } myDog(dog, ['name']); //["二哈"] myDog(dog, ['name', 'value']); //["二哈", "奥里给"] myDog(dog, ['key']); //错误,类型不匹配
映射类型
有时候我们可以会遇到这种情况,把每个成员都变为可选或者只读:
interface Person{ name: string; agent: number; } interface PersonPartial { name?: string; age?: number; } interface PersonReadonly { readonly name: string; readonly age: number; }
这在JavaScript里经常出现,TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。
interface IPerson{ name: string; agent: number; } type OPartial<T> = { [P in keyof T]?: T[P]; } type OReadonly<T> = { readonly [P in keyof T]: T[P]; } //使用方式 type PersonPartial = OPartial<IPerson>; type ReadonlyPerson = OReadonly<IPerson>;
参考
https://github.com/zhongsp/Ty…
https://github.com/jkchao/typ…
最后
文中有些地方可能会加入一些自己的理解,若有不准确或错误的地方,欢迎指出~
原文地址:https://segmentfault.com/a/1190000021792440