likes
comments
collection
share

Typescript学习(七)泛型的应用

作者站长头像
站长
· 阅读数 11

类型工具中的泛型

前面我们介绍了许多类型工具, 比如: 类型别名、索引类型、映射类型 等等; 而泛型往往会和他们组合在一起, 形成一些工具类型! 所谓的工具类型, 其实就像我们日常Javascript开发中的util.js中的一些工具方法, 它们往往是一些为了应对特定场景而封装好的方法, 在Typescript中, 工具类型就是这种方法, 而泛型, 就可以看作是这些方法的入參!

// 定义了一个工具类型AllBeString
type AllBeString<T> = {
  [P in keyof T]:string;
}
interface Person {
  name:string;
  age:number;
  height: number;
  weight: number;
}
// 将Person类型中的所有成员转为string类型
type NewPerson = AllBeString<Person>

/**
 * 
 * type NewPerson = {
 *  name: string;
 *  age: string;
 *  height: string;
 *  weight: string;
 *}
 */

以上案例中, AllBeString就像是一个函数, 它接受一个参数T, 在其内部逻辑中, 利用类型工具中的映射类型, 将T中的每个属性的类型变为string类型

除此之外, 还有一种被称为条件类型的类型工具, 能和类型别名能组成很多拥有非常有用的功能的工具类型, 先来看一个简单的案例, 了解它的基本特点:

type IsString<T> = T extends string ? true : false
type isStrType1 = IsString<123> // false
type isStrType2 = IsString<'hello'> // true

在上面的IsString这个类型工具中, 我们通过extends关键字来判断传入的泛型T是否属于string类型, 并以此为条件返回不同的结果, 这种就是条件类型, 条件类型主要用于限制约束泛型的类型, 使工具类型具有较强的灵活性;

在Typescript中, 其实有很多已经内置好了的的工具类型, 我们直接拿来用就行了, 它们种类繁多, 能适用于很多不同的场景, 但是它们的基本原理, 都是一样的, 都是利用类型工具的组合来实现的其逻辑:

type NonNullable<T> = T & {};
type K = NonNullable<string | number | null | undefined | boolean>
// 类型推导结果: type K = string | number | boolean

NonNullable为内置工具类型, 其组成非常简单, 即 类型别名 + 一个交叉类型; 这个工具类型作用是排除掉null和undefine类型; 由于在Typescript中, 任意类型和空对象组成的交叉类型, 都是这个类型本身, 但是, 推导过程中, 又会忽略掉null,undefined两个类型, 因此这个工具类型利用了此原理, 实现了剔除null和undefined的功能

还有非常常见的Record工具类型, 我们可以利用它来快速定义一个对象的类型, 它由类型别名+条件类型+索引类型查询 + 映射类型组成:

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

type Result = Record<string|number, string>
/**
 * type Result = {
 *  [x: string]: string;
 *  [x: number]: string;
 *}
*/

我们知道, keyof作为一个索引类型查询操作符, 可以获取一个类型的所有属性名或者一个索引类型的所有键名, keyof any则是获取所有可作为属性名/键名的类型组成的联合类型(string|number|symbol); 而条件类型在这里的作用就是规定泛型K, 必须符合string|number|symbol这个联合类型! 而在后续的映射类型中, 将K中的每一个成员都映射出来, 作为新对象的键/属性的类型;

默认泛型

前面我们介绍了泛型的基本使用, 我们知道, 泛型就像是一个函数的参数, 而函数的参数是由默认值的, 比如:

function firstCode (params = 'world') {
	return 'hello ' + params
}

既然如此, 泛型, 也应该拥有默认值

type MoreType<T = string> = T | number | symbol
type newType = MoreType // string | number | symbol

这样, MoreType已经有了默认的泛型, 后续使用的时候, 我们可以不用写明泛型了, 只需要一个工具类型即可

泛型约束条件

前面介绍过了条件类型这个类型工具, 我们知道, 它能够很好地约束泛型的类型, 如果泛型不符合某种条件(即 不是某个类型的子类型), 那Typescript将不执行对应的代码! A extends B, 即 A为B的子类型, 所谓子类型, 可以从2个角度理解: 更精确 或者 更多属性; 更精确, 通常指字面量类型比原始类型更精确, 如: 'hello world' extends string 成立, 即 字面量类型为原始类型子类型; 更多属性, 指的是子类除了拥有基类的属性, 还会具有自己的属性, 如: {name: string} extends {}, 具有具体属性的对象类型自然是空对象的子类型; 再来看几个案例

// 约束T必须是200 | 201 | 304的子类, 否则, 返回错误
type isSuccess<T> = T extends 200 | 201 | 304 ? 'success' : 'fail'

type myStatus = isSuccess<404> // fail

再来看个稍微复杂点的

class Animal {
  constructor (public color:string, age:number) {}
}

class Dog extends Animal {
  bite () {
    console.log('dog will bite you')
  }
}

class Car {
  run () {
    console.log('I can run')
  }
}

type isAnimal<T> = T extends Animal ? 'yes' : 'no'

type dogResult = isAnimal<Dog> // 'yes'

type carResult = isAnimal<Car> // 'no'

多泛型关联

一个工具类型可以接受多个泛型, 正如一个函数可以接受多个参数一样; 所以在Typescript中, 我们还能通过关联多个泛型, 使他们存在某种关联, 进而达到约束它们类型增强代码灵活性的目的:

function getProperty<K extends keyof T, T> (value:T, key: K):T[K] {
  return value[key]
}

getProperty({name: 'jack', age: 18}, 'name')

在上面的案例中, 我们通过条件类型来明确了K和T之间的关系, 即K其实是T的键的类型, 因此我们在实际调用函数过程中, key必须是value的属性成员! 所以, 当我们传了第一个参数{name: 'jack', age: 18}之后, 第二个参数只能在'name'和'age'两个值之间选择, 而不能随意输入任何值, 这就是泛型建立关联后产生的约束; 还有一个前后关联的典型场景就是选择省市区:

function getAddress<T, P extends keyof T, C extends keyof T[P], A extends keyof T[P][C]> (obj: T, province:P, city: C, area:A) {
  console.log(`${province as string}${city as string}${area as string}区`)
}

let addressObj = {
  '广东省': {
    '广州市': {
      '天河区': 'tianhe',
      '荔湾区': 'liwan'
    },
    '佛山市': {
      '顺德区': 'shunde',
      '南海区': 'nanhai'
    }
  }
}
getAddress(addressObj, '广东省', '佛山市', '南海区')

我们可以在泛型层面直接限定地理数据、 省、市、区三者的关系, 从而在我们调用getAddress的时候, 其参数的类型能够根据前面的参数实时改变, 增加了代码的灵活性;

对象中的泛型

我们在定义对象类型的时候, 同样可以使用到泛型, 前面我们都是使用类型别名举例子, 这里使用接口interface来实现下对象中使用泛型的场景

interface Person<T> {
  name:string;
  age:number;
  skill: T
}

let developer:Person<'code'|'debug'> = {
  name: '小明',
  age: 35,
  skill: 'code'
}

let soldiers:Person<'shoot'|'fire'|'run'>

let yezhichao:Person<'run'> = {
  name: '叶志超',
  age: 18,
  skill: 'run'
}

当然泛型还可以进行嵌套

type P = Promise<Person<'run'>>

function getTaobingInfo (params:Person<'run'>):P {
  return new Promise((resolve) => {
    resolve(params)
  })
}

getTaobingInfo(yezhichao).then(res=> {console.log(res)})

函数中的泛型

不仅对象中存在类型, 作为Javascript中非常重要的一份子, 函数自然也不能落后! 函数的泛型, 其实在前面的多泛型关联的案例中已经使用过了, 即前面的 getProperty以及getAddress方法, 函数中使用泛型的基本格式就是

function fn<T> (input:T):T {}

当然, 不是说返回值必须和参数类型一致, 这里只是展示函数中的泛型出现的位置, 即泛型的消费方, 很容易看出, 函数泛型的消费方, 就是参数和返回值; 此处就不再赘述案例了

类中的泛型

类中的泛型需要注意的是, 函数中的泛型消费方是参数/返回值, 而类中的消费方则是属性/方法

class Animal<T> {
  constructor(public name:string) {}
  public foods:T[] = []
  eat(food:T) {
    console.log(this.name + 'like' + food)
  }
}

const dog = new Animal<'bone'|'meat'>('dog')

dog.eat('bone')
dog.foods.push('bone')
dog.foods.push('meat')