TypeScript 泛型的使用实践记录 | 青训营

85 阅读3分钟

泛型介绍

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

function getValue<T>(value: T): T {
    return value;
}

在上面的代码示例中,我们创建了一个可以传入任意类型参数,并返回该参数的函数。泛型T起到了类型变量的作用,保证了传入值 value 的类型与函数返回值的类型相同。

  • 如果我们使用 number 替代 T ,那该函数就难以支持未来可能需要的各种数据类型。
  • 如果我们使用 any 替代 T,那该函数就无法保留传入参数原有的类型信息,无法保证传入值与返回值的类型相同。

由此可见,泛型允许我们使用相同的代码来处理不同类型的数据,使代码更灵活,可复用性更强。

泛型的使用示例

在我们的日常开发中,常常需要封装一个通用的请求函数,用以向不同的接口发送请求。然而不同的接口往往会返回不同类型的数据,这时就需要使用泛型处理不同类型的数据。

function request<T>(config: AxiosRequestConfig) {
  const data = ref(undefined) as Ref<T | undefined>;
  const error = ref(undefined) as Ref<string | undefined>;
  const fetching = ref(true);
  let whenFinish = Promise.resolve();
    
  // do something...
  
  return {
    data,
    error,
    fetching,
    whenFinish,
  }

在这个请求函数中,我们使用泛型 T 规定请求返回的数据类型,以此兼容不同接口的数据类型,保障请求数据的安全性。

代码中出现的 vue 响应式数据类型 Ref 的定义也是相同的道理,通过泛型规定 ref 对象的 value 值类型。

export interface Ref<T = any> {
    value: T;
    /**
     * Type differentiator only.
     * We need this to be in public d.ts but don't want it to show up in IDE
     * autocomplete, so we use a private Symbol instead.
     */
    [RefSymbol]: true;
}

通过extends规定类型范围

在默认情况下,泛型 T 可以代表任意类型,但有的时候我们只希望某一部分类型可以被传入函数中。这个时候,我们就需要使用 extends 规定泛型的类型范围。

export declare function ref<T extends Ref>(value: T): T;
export declare function ref<T>(value: T): Ref<UnwrapRef<T>>;
export declare function ref<T = any>(): Ref<T | undefined>;

vueref 函数定义中,通过函数重载的方式兼容了三种类型的参数。其中第一行的定义规定了在传入Ref对象参数时,直接返回该Ref类型的数据。在随后的重载中也兼容了传入响应式变量初始值和不传入初始值的两种情况。

利用泛型进行类型推导

借助泛型与 extends 三元表达式,我们可以完成一些简单的类型推导。

type LookUp<T, K> = T extends { type: K } ? T : never;
​
interface Cat {
  type: "cat";
  breeds: "Abyssinian" | "Shorthair" | "Curl" | "Bengal";
}
​
interface Dog {
  type: "dog";
  breeds: "Hound" | "Brittany" | "Bulldog" | "Boxer";
  color: "brown" | "white" | "black";
}
​
type MyDog = LookUp<Cat | Dog, "dog">; // expected to be `Dog`

在上述示例的LookUp 类型定义中,T extends { type: K } 进行逻辑判断,当 T 中的 type 属性为 K 时返回 true ,否则返回 false。所以,整个 LookUp 类型的作用是返回type 属性为 K的对象类型,若没有符合的对象类型,则返回 never

值得一提的是,联合类型进行三元表达式判断时,会将联合类型的各个子类型分开进行判断。故 type MyDog = LookUp<Cat | Dog, "dog"> 等价为 type MyDog = LookUp<Cat, "dog"> | LookUp<Dog, "dog">

总结

使用泛型可以显著提升 typescript 代码的可维护性,避免了大部分函数类型重载的场景,减少大量重复代码。同时,与 extends 关键字配合使用,可以完成各种灵活的类型定义,更优雅的利用 typescript 强大的类型推导功能。学会使用泛型之后,泛型替代了我曾经使用 any 的场景,显著提升了我代码的类型安全性。