Typescript 实战 --- (8)高级类型
1、交叉类型
将多个类型合并成一个类型,新的类型将具有所有类型的特性,适用于对象混用
语法:
类型1 & 类型2 & 类型3
interface CatInterface { run(): void } interface DogInterface { jump(): void } // 交叉类型具有所有类型的特性 let pet: CatInterface & DogInterface = { run() {}, jump() {} }
2、联合类型
声明的类型并不确定,可以为多个类型中的一个。用竖线(|)分隔每个类型,所以number | string | boolean表示一个值可以是number,string,或boolean
let a: number | string = 2; a = 'hello'; a = undefined; // 可以为其子类型 a = true; // Error: 不能将类型“true”分配给类型“string | number”
(1)、字面量联合类型:不仅限制类型,还限制取值
// 字符串联合类型 let x: 'typescript' | 'webpack' | 'nodejs'; x = 'webpack'; x = 'hello'; // Error: 不能将类型“"hello"”分配给类型“"typescript" | "webpack" | "nodejs"” // 数字联合类型 let y: 1 | 2 | 3; y = 3; y = 33; // Error: 不能将类型“33”分配给类型“1 | 2 | 3” let z: 'typescript' | 2; z = 'typescript'; z = 2; z = 1; // Error: 不能将类型“1”分配给类型“"typescript" | 2”
(2)、对象联合类型:在类型未确定的情况下,只能访问所有类型的公用成员
enum Pet { Dog, Cat }; interface DogInterface { run(): void; eat(): void; } interface CatInterface { jump(): void; eat(): void; } class Dog implements DogInterface { run() {}; eat() {}; } class Cat implements CatInterface { jump() {}; eat() {}; } function getPet(pet: Pet) { // let smallPet: Dog | Cat let smallPet = pet === Pet.Dog ? new Dog() : new Cat(); // 类型不确定时,只能取公有成员 smallPet.eat(); smallPet.run(); // 类型“Dog | Cat”上不存在属性“run” smallPet.jump(); // 类型“Dog | Cat”上不存在属性“jump” return smallPet; }
(3)、可区分的联合类型:这种模式从本质上来讲是结合了联合类型和字面量联合类型的一种类型保护方法
其核心思想是:如果一个类型是多个类型的联合类型,并且每个类型之间有一个公共的属性,那么就可以利用这个公共的属性创建不同的类型保护区块
// 例如:Shape是多个类型的联合类型,每个类型都具有一个公共属性kind,由此在 switch中建立了不同类型的保护区块 interface Rectangle { kind: 'rectangle'; width: number; height: number; } interface Square { kind: 'square'; size: number; } type Shape = Rectangle | Square; function area(s: Shape) { switch(s.kind) { case 'rectangle': return s.width * s.height; case 'square': return s.size * s.size; } }
如果又添加了一个联合类型,但是又没有在 area 函数中设定类型保护区块,会发生什么呢?
interface Rectangle { kind: 'rectangle'; width: number; height: number; } interface Square { kind: 'square'; size: number; } // 添加新的联合类型 interface Circle { kind: 'circle'; r: number; } type Shape = Rectangle | Square | Circle; function area(s: Shape) { switch(s.kind) { case 'rectangle': return s.width * s.height; case 'square': return s.size * s.size; } } console.log(area({ kind: 'circle', r: 1 })); // undefined
执行程序打印出了一个结果 undefined,由于上例中并没有在 area 方法中为 Circle 指定计算面积的方法,理论上应该提示错误,而不是直接返回 undefined。
为了让编译器正确的提示错误,有两种可选方法:
(1)、为 area 方法指定返回值类型
function area(s: Shape): number { switch(s.kind) { case 'rectangle': return s.width * s.height; case 'square': return s.size * s.size; } }
(2)、利用never类型
// 给定一个 default 分支,通过判断 s 是不是 never 类型来提示错误。 // 如果是 never 类型,则可以在前面的分支中找到对应的执行代码; // 如果不是 never 类型,则说明前面的代码有遗漏,需要补全 function area(s: Shape): number { switch(s.kind) { case 'rectangle': return s.width * s.height; case 'square': return s.size * s.size; default: return ((e: never) => { throw new Error(e) })(s); // 类型“Circle”的参数不能赋给类型“never”的参数 } }
通过错误提示补全代码
function area(s: Shape): number { switch(s.kind) { case 'rectangle': return s.width * s.height; case 'square': return s.size * s.size; case 'circle': return Math.PI * s.r ** 2 default: return ((e: never) => { throw new Error(e) })(s); } } console.log(area({ kind: 'circle', r: 1 })); // 3.141592653589793
3、索引类型
使用索引类型,编译器就能够检查使用了动态属性名的代码。例如:从js对象中选取属性的子集,然后建立一个集合
let obj = { a: 1, b: 2, c: 3 } function getValues(obj: any, keys: string[]) { return keys.map(key => obj[key]) } // obj 中存在的属性 console.log(getValues(obj, ['a', 'b'])); // [ 1, 2 ] // obj 中不存的属性,返回 undefined,而没有提示报错 console.log(getValues(obj, ['e', 'f'])); // [ undefined, undefined ]
索引类型可以用来解决上例中的问题,在认识索引类型之前需要先了解几个概念:
(1)、索引类型查询操作符 keyof T
对于任何类型T,keyof T 的结果是 类型T的所有公共属性的字面量的联合类型
interface Person { name: string; gender: string; age: number; } let personProps: keyof Person; // 'name' | 'gender' | 'age' console.log(personProps)
(2)、索引访问操作符 T[K]
类型T的属性K所代表的类型
interface Person { name: string; gender: string; age: number; } let n: Person['name']; // n 的类型是 string let a: Person['age']; // a 的类型是 number
(3)、泛型约束 T extends U
表示泛型变量可以继承某个类型获得某些属性
结合以上三点来改造 getValues 函数
// 1、用T来约束obj // 2、用K来约束keys数组 // 3、给K增加一个类型约束,让它继承obj的所有属性的联合类型 // 4、函数的返回值是一个数组,数组的元素的类型就是属性K对应的类型 let obj = { a: 1, b: 2, c: 3 } function getValues<T, K extends keyof T>(obj: T, keys: K[]): T[K][] { return keys.map(key => obj[key]) } // obj 中存在的属性 console.log(getValues(obj, ['a', 'b'])); // [ 1, 2 ] console.log(getValues(obj, ['e', 'f'])); // Error:不能将类型“string”分配给类型“"a" | "b" | "c"”
4、映射类型
通过映射类型,可以从一个旧的类型生成一个新的类型,比如把一个类型中的所有属性变成只读
interface Obj { a: string; b: number; c: boolean; }
4-1、同态
同态的意思是:只会作用于旧类型的属性,而不会引入新的属性
(1)、Readonly<T> 将旧类型中的每一个成员都变成只读
type ReadonlyObj = Readonly<Obj>;
(2)、Partial<T> 把旧类型中的每一个成员都变成可选的
type PartialObj = Partial<Obj>;
(3)、Pick<T, key1 | key2 | keyn> 可以抽取旧类型中的一些子集
接受两个参数:第一个是要抽取的对象,第二个是要抽取的属性的key
type PickObj = Pick<Obj, 'a' | 'c'>;
4-2、非同态,会创建一些新的属性
(1)、Record<key1 | key2 | keyn, T>
接受两个参数:第一个参数是一些预定义的新的属性,第二个参数是一个已知的对象
type RecordObj = Record<'x' | 'y', Obj>;
映射类型的本质是一些预定义的泛型接口,通常还会结合索引类型来获取对象的属性和属性值,从而将一个对象映射成想要的结构