10 泛型

166 阅读5分钟

面试两个问题:

  1. 如何使用TypeScript实现call(或者apply)功能类似的函数
  2. 什么是泛型? 泛型的作用是什么?

什么是泛型

泛型指的的参数类型话,并在调用时给泛型传入明确的类型参数。

设计泛型的目的在于有效约束类型成员之间的关系, 比如函数的参数和返回值,类或者接口成员和方法之间的关系。

泛型化类型参数

泛型最常用的场景是用来约束函数参数的类型,给函数定义若干个被调用时才会传入明确类型的参数。

function reflect<P>(param: P): P {
    return param;
}

const refectStr = reflect<string>('string'); // str 类型时string
const reflectNum = reflect<number>(1); // num类型时number

如果调用泛型函数时受泛型约束的参数有传值, 泛型参数的入参可以从参数的类型中进行推断,而无需再显示指定类型(可缺省), 因此, 上述调用可以简写:

const reflectStr = reflect('string'); // str类型是string
const reflectNum2 = reflect(1); //num类型时number

泛型不仅可以约束函数整个参数的类型,还可以约束参数属性,成员的类型,比如参数的类型可以是数组,对象。如下:

function reflectArray<P>(param: P[]){
    return param
}

const flectArr = reflectArray([1, '1']); // reflectArr是(string| number), 即推导出来的P的值

示例, React Hooks中:

function useState<S>(state: S, initialVal?: S){
    return [state, (s: S) => void 0] as unknown as [S, (s: S) => void]
}

注意, 函数的发耐性如蝉必须和参数、参数成员简历有效的约束关系才有实际意义。

泛型类

我们可以使用泛型来约束构造函数,属性,方法的类型。

class Memory<S> {
    store: S;
    constructor(store: S){
        this.store = store;
    }
    
    set (store: S) {
        this.store = store;
    }
    get() {
        return this.store;
    }
}


const numMemory = new Memory<number>(1); // <number> 可缺省
const getNumMemory = numMemory.get(); // 类型是number
numMemory.set(2); // 只能写入number类型

const strMemory = new Memory(''); // 缺省<string>
const getStrMemory = strMemory.get();// 类型是string

strMemory.set('string'); //只能写入string类型

首先, 定义一个支持读写的寄存器类Memory, 并使用泛型约束了Memory类的构造器函数, set个get方法形参的类型, 最后实例化了泛型入参分别是number和string类型的两种寄存器。

泛型类型

在TypeScript中, 类型本身就可以被定义为拥有不明确的类型参数的泛型, 并且可以接收明确类型作为入参, 从而衍生出更具体的类型, 如下代码所示:

const reflectFn: <P>(param:P)=> P = reflect

我们可以将reflectFn的类型注解提取为一个能被复用的类型别名或者接口, 如下:

type ReflectFunction = <P>(param: P) => P;
interface IReflectFunction() {
    <P>(param: P): P
}


const reflectFn2: ReflectFunction = reflect;
const reflectFn3: IReflectFunction = reflect;

将类型入参的定义移动到类型别名或者接口名称后, 此时定义的一个接收具体类型入参后返回一个新的类型的类型就是泛型类型。

如下, 我们定义了两个可以接收入参P的泛型类型

type GenericReflectFunction<P> = (param: P) => P;
interface IgenericReflectFunction<P> {
    (param: P): P;
}

const reflectFn4: GenericReflectFunction<string> = reflect; // 具象化泛型
const reflectFn5: IGenericReflectFunction<number> = reflect; // 具象化泛型

const reflectFn3Return = reflectFn4('str');// 入参和返回值必须是string类型
const reflectFn4Return = reflectFn5(1); // 入参和返回值都必须是number

在泛型定义中, 我们可以使用一些类型操作符进行运算表达, 使得泛型可以根据入参的类型衍生出各异的类型,如下代码所示:

type StringOrNumberArray<E> = E extends string | number ? E[] : E;

type StringArray = StringOrNumberArray<string>; // string[]
type NumberArray = StringOrNumberArray<number>; // number[]

type NeverGot = StringOrNumberArray<boolean>; // boolean

分配条件 Distributive Conditional Types

注意看WhatIsThisBooleanOrStringGot类型:

type StringOrNumberArray<E> = E extends string | number ? E[] : E;

type BooleanOrString = string | boolean;

type WhatIsThis = StringOrNumberArray<BooleanOrString>;  // boolean | string[]

type BooleanOrStringGot = BooleanOrString extends string | number
    ? BooleanOrString[]
    : BooleanOrString;  //  string | boolean

解释:

在条件类型判断的情况下(比如上边实例中出现的extends),如果入参是联合类型,则会被拆解成一个独立的(原子)类型(成员)进行类型运算。

上例中的string | boolean入参, 先被拆解成string和boolean这两个独立类型, 在分别判断是否是 string | number类型的子集。 因为string是子集而boolean不是,所以最终我们得到的WhatIsThis的类型是 boolean |string[]

泛型约束

泛型就像是类似的函数, 它可以抽象,封装并接收(类型)入参,而泛型的入参也拥有类似函数入参的特性。 因此, 我们可以把泛型入参限定在一个相对更明确的集合内, 以便对入参进行约束。 可以使用泛型入参名extends类型语法达到这个目的。 如下:

// 限定泛型入参只能是number|string|boolean的子集
function reflectSpecified<P extends number | string | boolean>(param: P): P {
    return param;
}

我们还可以在多个不同的泛型入参之间设置约束关系


interface ObjSetter {
    <O extends {}, K extends Keyof O, V extends O[K]>(obj: O, key: K, value:V)
}

const setValueOfObj : ObjSetter = (obj, key,value) => (obj[key] = value);

setValueOfObj({id:1, name: 'name'}, 'id', 2); // ok
setValueOfObj({id: 1, name: 'name'}, 'name', 'mew mame'); // ok
setValueOfObj({id:1, name: 'name'}, 'age', 2); //ts 2345
setValueOfObj({id:1, name: 'name'}, 'id', '2'); //ts 2345

泛型入参默认值

泛型入参与函数入参还有一个相似的地方在于,它也可以给泛型入参指定默认值(默认类型),且语法和指定函数默认参数完全一致, 如下:

interface ReduxModelSpecified<State extends {id: number; name: string}> {
    state: State
}

interface ReduxModelSpecified2<State = {id: number; name: string}> {
    state: State
}

type ComputedReduxModel5 = ReduxModelSpecified2; // ok
type ComputedReduxModel6 = ReduxModelSpecified2<{id: number; name: string;}>; // ok
type ComputedReduxModel7 = ReduxModelSpecified; // ts 2314 缺少一个类型参数

上述定义了入参有默认类型的泛型ReduxModelSpecified2, 因此使用ReduxModelSpecified2时类型入参可缺省。 而ReduxModelSpecified的入参没有默认值, 所以缺省入参时会提示一个类型错误。

默认值与约束组合使用

interface ReduxModelMixed<State extends {} = {id: number; name: string}> {
    state: State
}