TypeScript中的泛型使用指南

9/1/2021 Typescript

为了提高接口和组件的可重用性,可以使用泛型的方法,这样接口或组件就能支持多种类型的数据了,是一种十分灵活的功能。

泛型的名字可以随便,但一般约定是大写,或者常用的定义

  • K(Key):表示对象中的键类型;
  • V(Value):表示对象中的值类型;
  • E(Element):表示元素类型。
  • T(Type):表示类型

# 一. 在函数中使用

在函数中指定泛型,就能限制传入的参数的类型,以及函数返回值,后续就有约束

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

console.log(identity<number>(1)) // 1
console.log(identity<string>("myString")) // myString

# 二. 在接口或类中使用

在接口和类中使用泛型,就可以约束其中的属性,方法和继承类

可以通过逗号隔开来定义多个泛型,例如接口定义。

interface Identities<V, M> {
  value: V,
  message: M
}

也可以把泛型直接传入继承的接口或类中,就可以把同一个泛型传递到继承类中。

interface GenericInterface<U> {
  value: U
  getIdentity: () => U
}

class IdentityClass<T> implements GenericInterface<T> {
  value: T

  constructor(value: T) {
    this.value = value
  }

  getIdentity(): T {
    return this.value
  }
}

const myNumberClass = new IdentityClass<Number>(68);
console.log(myNumberClass.getIdentity()); // 68

const myStringClass = new IdentityClass<string>("Semlinker!");
console.log(myStringClass.getIdentity()); // Semlinker!

接下来我们以实例化 myNumberClass 为例,来分析一下其调用过程:

  • 在实例化 IdentityClass 对象时,我们传入 Number 类型和构造函数参数值 68
  • 之后在 IdentityClass 类中,类型变量 T 的值变成 Number 类型;
  • IdentityClass 类实现了 GenericInterface<T>,而此时 T 表示 Number 类型,因此等价于该类实现了 GenericInterface<Number> 接口;
  • 而对于 GenericInterface<U> 接口来说,类型变量 U 也变成了 Number。这里我有意使用不同的变量名,以表明类型值沿链向上传播,且与变量名无关。

# 三. 在集合中使用

在Array,Map,Set中可以指定集合中的元素是什么类型。

const arr = new Array<string>();
const map = new Map<string, number>();
const set = new Set<string>();

# 四. 泛型约束

  • 继承类型

    interface Length {
      length: number;
    }
    
    function identity<T extends Length>(arg: T): T {
      console.log(arg.length); // 可以获取length属性
      return arg;
    }
    
  • 检查对象上的键是否存在

    这里会用到一个keyof操作符,keyof操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。

    interface Person {
      name: string;
      age: number;
      location: string;
    }
    
    type K1 = keyof Person; // "name" | "age" | "location"
    type K2 = keyof Person[];  // number | "length" | "push" | "concat" | ...
    type K3 = keyof { [x: string]: Person };  // string | number // key只能string或者number, number会自动转成string
    type K3 = keyof { [x: number]: number }; // number
    

    通过keyof操作符获取传入泛型的所有属性并作为K泛型,这样传入的key必须是T泛型的属性。

    enum Difficulty {
      Easy,
      Intermediate,
      Hard
    }
    
    function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
      return obj[key];
    }
    
    let tsInfo = {
       name: "Typescript",
       supersetOf: "Javascript",
       difficulty: Difficulty.Intermediate
    }
     
    let difficulty: Difficulty = 
      getProperty(tsInfo, 'difficulty'); // OK
    
    let supersetOf: string = 
      getProperty(tsInfo, 'superset_of'); // Error
    

# 五. 默认泛型

直接指定<T = Default Type>

interface A<T=string> {
  name: T;
}

const strA: A = { name: "Semlinker" };
const numB: A<number> = { name: 101 };

泛型参数的默认类型遵循以下规则:

  • 有默认类型的类型参数被认为是可选的。
  • 必选的类型参数不能在可选的类型参数后。
  • 如果类型参数有约束,类型参数的默认类型必须满足这个约束。
  • 当指定类型实参时,你只需要指定必选类型参数的类型实参。 未指定的类型参数会被解析为它们的默认类型。
  • 如果指定了默认类型,且类型推断无法选择一个候选类型,那么将使用默认类型作为推断结果。
  • 一个被现有类或接口合并的类或者接口的声明可以为现有类型参数引入默认类型。
  • 一个被现有类或接口合并的类或者接口的声明可以引入新的类型参数,只要它指定了默认类型。

# 六. 条件类型

https://www.tslang.cn/docs/release-notes/typescript-2.8.html

利用三元运算符来判断返回什么类型,其中extends用于判断是否符合结构兼容性

T extends U ? X : Y

这个意思是 T是否赋值给U(或者T包含U),那么类型就是X,否则类型是Y

条件类型经常配合infer来使用, infer的作用:可以在 extends 条件语句中使用 infer 关键字来声明一个待推断的类型变量

interface Dictionary<T = any> {
  [key: string]: T;
}
 
type StrDict = Dictionary<string>

type DictMember<T> = T extends Dictionary<infer V> ? V : never
type StrDictMember = DictMember<StrDict> // string

上面的意思是T能赋值,或者T包含或者等于 Dictionary,就把Dictionary<type>中的type作为类型返回值,否则就是never

async function stringPromise() {
  return "Hello, Semlinker!";
}

interface Person {
  name: string;
  age: number;
}

async function personPromise() {
  return { name: "Semlinker", age: 30 } as Person;
}

type PromiseType<T> = (args: any[]) => Promise<T>;
type UnPromisify<T> = T extends PromiseType<infer U> ? U : never;

type extractStringPromise = UnPromisify<typeof stringPromise>; // string
type extractPersonPromise = UnPromisify<typeof personPromise>; // Person

通过infer和条件类型来获取Promise的return值

# 七. Typescript常用的泛型工具类型

Typescript已经帮我们定义了一些常用的泛型。

  • Partial — 将某个类型里的属性全部变成可选? in的作用是遍历T的所有Key

    /**
     * node_modules/typescript/lib/lib.es5.d.ts
     * Make all properties in T optional
     */
    type Partial<T> = {
        [P in keyof T]?: T[P];
    };
    
    // 用法
    interface Todo {
      title: string;
      description: string;
    }
    
    const another: Todo = {
      title: 'test'
    } // Error
    
    const todo: Partial<Todo> = {
    	title: 'test'
    }
    
  • Record — 作用是将 K 中所有的属性的值转化为 T 类型

    /**
     * node_modules/typescript/lib/lib.es5.d.ts
     * Construct a type with a set of properties K of type T
     */
    type Record<K extends keyof any, T> = {
        [P in K]: T;
    };
    
    //用法
    interface PageInfo {
      title: string;
    }
    
    type Page = "home" | "about" | "contact";
    
    const x: Record<Page, PageInfo> = {  // 属性只能是Page, PageInfo
      about: { title: "about" },
      contact: { title: "contact" },
      home: { title: "home" }
    	test: { description: 'description' } // Error
    };
    
  • Pick — 作用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。

    // node_modules/typescript/lib/lib.es5.d.ts
    
    /**
     * From T, pick a set of properties whose keys are in the union K
     */
    type Pick<T, K extends keyof T> = {
        [P in K]: T[P];
    };
    
    //用法
    interface Todo {
      title: string;
      description: string;
      completed: boolean;
    }
    
    type TodoPreview = Pick<Todo, "title" | "completed">;
    
    const todo: TodoPreview = {
      title: "Clean room",
      completed: false,
    	description: 'test' // Error
    };
    
  • Exclude — 作用是将某个类型中属于另一个的类型移除掉, 如果T能赋值给U(T包含U)返回never表示移除,否则就返回T,一般用 | 或者 &结合Exclude使用

    // node_modules/typescript/lib/lib.es5.d.ts
    
    /**
     * Exclude from T those types that are assignable to U
     */
    type Exclude<T, U> = T extends U ? never : T;
    
    // 用法
    type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
    type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
    type T2 = Exclude<string | number | (() => void), Function>; // string | number
    
  • ReturnType — 作用是用于获取函数 T 的返回类型。

    // node_modules/typescript/lib/lib.es5.d.ts
    
    /**
     * Obtain the return type of a function type
     */
    type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
    
    // 用法
    type T0 = ReturnType<() => string>; // string
    type T1 = ReturnType<(s: string) => void>; // void
    type T2 = ReturnType<<T>() => T>; // {}
    type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]
    type T4 = ReturnType<any>; // any
    type T5 = ReturnType<never>; // any
    type T6 = ReturnType<string>; // Error
    type T7 = ReturnType<Function>; // Error