31

TypeScript 从入门到放弃(二):泛型、高级类型

 4 years ago
source link: https://blog.itlee.top/2019/11/12/TypeScript-从入门到放弃-二-泛型、高级类型/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK