likes
comments
collection
share

你必须掌握的 Typescript 常见类型操作——泛型

作者站长头像
站长
· 阅读数 17
你必须掌握的 Typescript 常见类型操作——泛型

泛型主要是为了解决类型结构复用的问题。可以说泛型给了你在使用 ts 类型检测的体验同时,又提供了很好的类型扩展性、可维护性。

在使用泛型类型时,可以将泛型视为参数传给类型对象。在 ts 中 Array 就是一个非常常见的泛型类型。

声明一个字符串数组:

let stringList: string[] = ['a', 'b']

使用泛型声明:

let stringList: Array<string> = ['a', 'b']

泛型可以被使用在类型别名、接口、函数、类中。

泛型函数

使用 ts 写一个可以处理各种类型数组遍历的函数,如果不使用泛型,你可能需要单独多个类型的数组遍历操作。或者通过函数重载来实现,想想都麻烦~。如果使用泛型的话,如下。

function forEach<T>(array: T[]): void {
  for(let i = 0; i < array.length; i++) {
    console.log(array[i])
  }
}
// number 类型
forEach<number>([1,2,3,4,5])
// string 类型
forEach<string>(['a','b','c','d','e'])

通过上面泛型遍历函数的示例,可以知道,泛型就是一个类型可以接受另一个类型作为参数使用,只有在调用函数或者类型的时候才需要确定下具体的类型。在 ts 中,需要使用 <> 括号,作为泛型参数的承接对象。

泛型类型推断

还是以上面的函数法为例:

forEach([false, true, false]) // ok

在调用 forEach 的时候并没有传递泛型类型。但是 ts 编译结果正常通过。这是因为 ts 会根据参数的类型推断出一个类型作为泛型使用。这个特性使我们的代码更加简洁。

泛型类型

  • 声明一个泛型类型
type Robot<T> = {
  function: T[],
  weigh: number,
  price: number
}

const wali: Robot<string> = {
  function: ['搬运', '说话']
  weigh: 100,
  price: 100,
}
  • 声明一个泛型接口
interface Robot<T> {
  function: T[],
  weigh: number,
  price: number
}
const wali: Robot<string> = {
  weigh: 100,
  price: 100,
  function: ['搬运', '说话']
}
  • 使用函数泛型类型,改写上面的 forEach 函数:
function forEach<T>(array: T[]): void {
  for(let i = 0; i < array.length; i++) {
    console.log(array[i])
  }
}

// 使用匿名类型
const myForeach: <T>(array: T[]) => void = forEach
  • 使用匿名类型:
// 使用匿名类型
const myForeach: { <T>(array: T[]): void } = forEach
  • 使用类型别名:
type Foreach = {
  <T>(array: T[]): void
}
const myForeach: Foreach = forEach
  • 使用 interface:
interface Foreach {
  <T>(array: T[]): void
} 
const myForeach: Foreach = forEach

注意上面通过 type、interface 创建的函数类型并没有在类型名称旁边通过 <> 传递泛型。

通过上面几个示例,可以知道泛型在函数或者对象中的使用方式

  • 传递多个泛型
function forEach<T, R>(array: T[], handle: (item: T) => R): void {
  for(let i = 0; i < array.length; i++) {
    handle(array[i])
  }
}

泛型类

使用泛型创建一个泛型类。

class GenericNumber<NumType> {
  zeroValue: NumType;
  add: (x: NumType, y: NumType) => NumType;
}
 
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

泛型约束

当你使用泛型类型的时候,你会发现你可以将泛型视为任何类型。这就会导致有些超出预期的错误。

例如想写一个可以读取任意类型数组 length 属性的泛型函数:

function arrayLength<T>(array: T): number {
  return array.length
}

你会发现编译器会提示出错。我们在写的时候,内心假定的 array 是一个数组。但是 ts 编译器会将 T 泛型视为任意类型。这就意味具体调用 arrayLength时, array 可能是非数组类型,并没有 length 属性。这时你可以这么写:

function arrayLength<T>(array: T[]): number {
  return array.length
}

或者说声明一个接口类型,通过接口类型描述我们对泛型的属性约束,并使用 extends 关键字对泛型进行约束。

interface List {
  length: number;
}
function arrayLength<T extends List>(array: T[]): number {
  return array.length
}

extends 关键字在 ES6 中,可以用于类之间的继承。而在 ts 中 extends 关键字不仅可以作用于类之间的继承,还可以:

  • 作用于接口类型之间的继承
interface Animal {
  name: string;
  heigh: number;
  weigh: number;
}

interface Person extends Animal {
  language: string
}

这时 Person 类型会拥有 Animal 类型的所有属性。

  • 结合接口类型,对泛型进行约束
interface Animal {
  name: string;
  heigh: number;
  weigh: number;
}

function animalName<T extends Animal>(animal: T): string {
  return animal.name
}
  • 结合三元运算符,进行类型判断
interface Animal {
  name: string;
  heigh: number;
  weigh: number;
}

interface Person extends Animal {
  language: string
}

type isPerson = Person extends Animal ? true : false // true

使用类型参数对泛型进行约束

在 ts 中你可以使用泛型参数对另一个泛型进行约束。

function getKey2ValueMap<T, Key extends keyof T>(obj: T, key: Key): T[keyof T] {
  return obj[key]
}

interface Animal {
  name: string;
  heigh?: number;
  weigh?: number;
}

const dog: Animal = {
  name: '八公'
}

getKey2ValueMap(dog, 'name') // Ok
getKey2ValueMap(dog, 'heigh') // Ok
getKey2ValueMap(dog, 'age') // Error

上面示例中,我们通过 Key extends keyof T 对泛型 Key ,进行了类型约束。keyof T 操作会返回所有 T 类型的属性的联合类型,这会将 key 约束在 T 的属性类型范围之中。

在泛型描述 class 类型

当你使用工厂模式时,需要使用泛型函数接受一个泛型类来实现。

  • 使用匿名类型:
function createFactory<Type>(c: { new(): Type }): Type {
  return new c();
}
  • 使用类型别名创建泛型类:
type Animal = {
  new<T>(): T
}

function createFactory<Type>(c: Animal): Type {
  return new c();
}
  • 使用接口创建泛型类:
interface Animal {
  new<T>(): T
}

function createFactory<Type>(c: Animal): Type {
  return new c();
}

在 ts 中使用 class 关键字创建的类,也是一种类型对象,可以直接当做类型对象,对实例对象进行约束。

class Animal {
  count: number = 10
}
const animal: Animal = new Animal()

在处理类之间的继承关系时,则可以通过类类型对类实例进行约束:

class BeeKeeper {
  hasMask: boolean = true;
}
 
class ZooKeeper {
  nametag: string = "Mikle";
}
 
class Animal {
  numLegs: number = 4;
}
 
class Bee extends Animal {
  keeper: BeeKeeper = new BeeKeeper();
}
 
class Lion extends Animal {
  keeper: ZooKeeper = new ZooKeeper();
}
 
function createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}
 
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;

总结

泛型可以极大提高类型系统的可复用性。在使用泛型时可以结合 extends、keyof、typeof 关键字操作,产生更加抽象和多变的类型模式。学会了就赶紧用起来吧。

参考:

转载自:https://juejin.cn/post/7235966403064725564
评论
请登录