摘要:// 使用 泛型 定义函数类型别名 type Log = (value: T) => T //定义泛型别名 let myLog: Log = log // 泛型在接口中使用 // 等价于 别名 // 也可以指定默认类型 interface ILog { // 指定默认泛型类型。还可以通过泛型约束,灵活控制类型。

本文作为学习笔记,文中内容大多来自官方文档和一些资料,摘抄的部分会在文中标注出原文地址,可以直接参考原文。

上一篇学习了 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 的属性而没有其它的。

相关文章