TypeScript 从入门到放弃(二):泛型、高级类型
摘要:// 使用 泛型 定义函数类型别名 type Log =
本文作为学习笔记,文中内容大多来自官方文档和一些资料,摘抄的部分会在文中标注出原文地址,可以直接参考原文。
上一篇学习了 TS 的基本数据类型、接口、函数和类等基本用法。接下来继续深入学习一些相对 JS 来说 TS 中新增的内容。
泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
下面通过一个例子来了解泛型,如何实现一个打印函数呢?
function log (value: string): string { console.log(value) return value }
上面的这个 log
方法,只能接收和返回 string
类型数据。如何才能让该方法接收和返回 string[]
类型的数据呢?
第一种方法:函数重载,通过重载可以动态的匹配符合的类型。
function log (value: string): string; function log (value: string[]): string[] function log (value: any): any { console.log(value) return value }
第二种方法:联合类型
function log (value: string | string[]): string | string[] { console.log(value) return value }
联合类型会比函数重载会简洁一点。如果前面学的比较扎实,相信会想到 any 类型。
第三种方法:any 类型
function log (value: any): any { console.log(value) return value }
这种方式比联合类型和函数重载都简洁,但是存在一个问题会丢失信息,即传入的类型和返回的类型应该相同。
如何优雅的解决这个问题呢?就是泛型,它可以不指定参数和返回值的类型,只有在真正使用的时候才去确定。
function log<T> (value: T): T { console.log(value) return value } log<string>('a') // 类型可以省略 log(['a', 'b']) // 省略 <string[]>
这种方式是不是更简洁,这只是基本用法。
TS 中的高级类型也广泛使用了泛型。如,泛型定义函数别名、泛型接口等。
// 使用 泛型 定义函数类型别名 type Log = <T>(value: T) => T //定义泛型别名 let myLog: Log = log // 泛型在接口中使用 // 等价于 别名 // 也可以指定默认类型 interface ILog<T = string> { // 指定默认泛型类型。 (value: T): T; } // 指定类型。 let myILog: ILog<number> = log
泛型类和泛型约束
泛型类和泛型接口的写法差不多,在类名后面加 <>
指定泛型类型。
// 泛型类 class CLog<T> { run (value: T) { console.log(value) return value } } let clog1 = new CLog<number>() clog1.run(10) // 10 let clog2 = new CLog() clog2.run(['a', 'b']) // ['a','b']
类可以支持多种类型,增强程序的扩展性。还可以通过泛型约束,灵活控制类型。
// 泛型约束 interface Length { length: number } // 有时需要获取泛型中的一个属性,但是编译器不知道有没有 length 属性。 // 通过继承 Length 接口添加泛型约束。 function sLog<T extends Length> (value: T): T { console.log(value, value.length) return value } sLog([1]) // [1] 1 sLog('123') // 123 3 sLog({ length: 1 }) // {length: 1} 1
通过接口 Length
约束泛型。创建一个包含 .length
属性的接口,使用这个接口和 extends
关键字来实现约束。
小结一下
使用泛型有什么优点呢?
- 可以动态支持类型,增强程序的扩展性。
- 可以替代重载和联合类型声明,提高代码的简洁性。
- 泛型约束,灵活控制类型间的约束。
高级类型
交叉类型
所谓的交叉类型是将多个类型合并为一个类型。将现有的多个类型叠加在一起,它包含所需的所有类型的特性。使用 &
符合。
举个例子:
interface DogInterface { run (): void } interface CatInterface { jump (): void } // 定义对象实现交叉类型接口,run 和 jump 都必须实现。 let pet: DogInterface & CatInterface = { run () { }, jump () {}, }
PS: 交叉类型取的是所有类型的并集,而不是交集。
联合类型
前面也学到了联合类型,联合类型取得是两者中的一个。
class ADDog implements DogInterface { run () { } eat () {} } class ADCat implements CatInterface { jump () { } eat () {} } enum Master { Boy, Girl } function getPet (master: Master) { // 此时 pet 是联合类型 let pet = master === Master.Boy ? new ADDog() : new ADCat() pet.eat() // pet.run() // error return pet }
PS:联合类型取得所有类型的交集。这里取的 Dog | Cat
两者的交集。
可区分的联合类型
通过一个公共的字面量区分不同的类型。
interface Square { kind: 'square', // 表示类型 size: number } interface Rectangle { kind: 'rectangle', width: number, height: number } interface Circle { kind: 'circle', r: number } type Shape = Square | Rectangle | Circle function area (s: Shape) { switch (s.kind) { // 共有属性 case 'square': // 不同的类型保护区块。 return s.size * s.size case 'rectangle': return s.width * s.height case 'circle': return Math.PI * s.r ** 2 default: // s为never类型,表示前面的分支都被覆盖。 // s不是never类型,说明前面分支有遗漏。 return ((e: never) => { throw new Error(e) })(s) } } // 问题:新加kind时,存在问题。 console.log(area({ kind: 'circle', r: 1 })) // 输出: Undefined
索引类型
使用索引类型,编译器就能够检查使用了动态属性名的代码。
看一个 JS 例子。
let obj = { a: 1, b: 2, c: 3 } // 对象中选取属性值的子集 function getValues (obj: any, keys: string[]) { return keys.map(key => obj[key]) } console.log(getValues(obj, ['a', 'b'])) // [1, 2] console.log(getValues(obj, ['e', 'f'])) // 并不会提示属性缺失。['undefined', 'undefined']
使用索引类型可以添加类型约束改造,需要使用 索引类型查询 和 索引访问 操作符。
先看一下使用 TS 实现后结果。
// 泛型 T 约束 Obj;泛型 K 约束数组。 // K 继承自 T 所有属性的联合类型。 function getValuesTS<T, K extends keyof T> (o: T, names: K[]): T[K][] { return names.map(key => o[key]) } console.log(getValuesTS(obj, ['a', 'b'])) // console.log(getValuesTS(obj, ['e', 'f'])) // error, 编译器报错。
编译器会检查数组中的元素是否是 obj
的一个属性。需要注意的地方,
keyof T
索引类型查询操作符。对任何类型的 T, keyof T
的结果为 T 上已知的公共属性名的联合。
// keyof T interface Obj { a: number, b: string } let key: keyof Obj // key 是 a | b 类型的。
T[K]
索引访问操作符。作用就是 o[key]
具有的类型就是 obj['a']
,在普通上下文中使用 T[K]
就像使用索引类型查询一样。
映射类型
有时需要将已知类型的每个属性变成可选的或者只读的。TS 提供类从旧类型中创建新类型的一种方式: 映射类型 。
// 接口 Person interface Person { name: string; age: number; score: number } // 映射为只读类型 type ReadonlyPerson = Readonly<Person> // 可选 type PartialPerson = Partial<Person> // 选取部分类型 type PickPerson = Pick<Person, 'name' | 'age'>
Readonly<T>
、 Partial<T>
、 Partial<T>
是 TS 库内置的映射类型。还有很多其他的映射类型,以上三种类型:属性列表中的 keyof T 且结果类型是 T[P] 的变体。这种转换称为同态,映射只作用于 T 的属性而没有其它的。