TypeScript(十)泛型进阶

本文最后更新于:6 个月前

目录

前言

泛型约束

联合类型+泛型约束

交叉类型+泛型约束

泛型约束泛型

递归类型别名

条件类型

分发条件类型

类型过滤

类型推导

infer关键字

回到类型推导

映射&索引类型

索引访问类型

映射类型

必选属性

可变属性

结语

相关文章


前言

本文收录于TypeScript知识总结系列文章,欢迎指正!

上篇文章我们领略了泛型的灵活及强大;了解了泛型的基本使用以及常见用法。本文将针对泛型的其他用法做一些进阶拓展,其中有许多知识点可以放在前面的文章介绍,但是与泛型放在一起可能更好理解,那么话不多说,直接开始

泛型约束

我们可以通过泛型约束来对泛型参数类型进行限制,确保它符合特定的类型要求,泛型约束的写法是在关键字extends后追加类型来实现,约束的类型可以是任意类型下面是一个简单的例子

type Animal<T extends hobbyList> = {
    name: string
    hobby: T
}
type hobbyList = {
    length: number
    push: (...args: any[]) => number
}
const animal: Animal<hobbyList> = {
    name: "阿黄",
    hobby: ['ball', 'flying-disc']
}
animal.hobby.push("run")

console.log(animal.hobby); //  [ 'ball', 'flying-disc', 'run' ]

可以看到,上面的函数中我们限制了animal传入的泛型T有且仅包含length属性及push方法,此时我们对animal中的hobby修改类型或者调用其他方法时就会报错

animal.hobby.concat(["run"]) // 类型“hobbyList”上不存在属性“concat”
animal.hobby = {} // 类型“{}”缺少类型“hobbyList”中的以下属性: length, push

联合类型+泛型约束

之前的文章中我们接触到了联合类型,在泛型的约束中同样可以应用该场景中,表示泛型类型参数必须是多个类型中的一种,如

type Animal<T extends string | string[]> = {
    name: string
    hobby: T
}
const animal: Animal<string> = {
    name: "阿黄",
    hobby: "ball"
}
const animal2: Animal<string[]> = {
    name: "阿黄",
    hobby: ['ball', 'flying-disc']
}

交叉类型+泛型约束

除了上面的联合类型外,我们还可以使用交叉类型来限制泛型同时满足多个类型特征,比如

type Animal<T extends arrayLength & arrayPush> = {
    name: string
    hobby: T
}
type arrayLength = {
    length: number
}
type arrayPush = {
    push: (...args: any[]) => number
}
type MyArray<T> = arrayLength & arrayPush & {
    forEach: (cb: (item: T, i: number, arr: MyArray<T>) => void) => void
}
const animal: Animal<MyArray<string>> = {
    name: "阿黄",
    hobby: ['ball', 'flying-disc']
}
animal.hobby.push("run")
console.log(animal.hobby.length); // 3

上面的代码中type MyArray同时拓展了arrayPush,arrayLength两个接口,并在其基础上新增了forEach方法,此时是可以正确赋值给Animal的泛型的

泛型约束泛型

所谓泛型约束泛型,就是在泛型类型参数中使用其他泛型类型参数来约束它的类型,有点套娃的意思,比如我们对上面的代码做一些修改:在Animal中增加一个getHobby的属性,这个属性类型是U(即arrayLength),而T是被约束于U的,所以T依旧可以取MyArray,简单的说就是T类型是U类型的子类,子类的属性是只能多不能少的

type Animal<T extends U, U> = {
    name: string
    hobby: T
    getHobby: U
}
type arrayLength = {
    length: number
}
type arrayPush = {
    push: (...args: any[]) => number
}
type MyArray<T> = arrayLength & arrayPush & {
    forEach: (cb: (item: T, i: number, arr: MyArray<T>) => void) => void
}
const animal: Animal<MyArray<string>, arrayLength> = {
    name: "阿黄",
    hobby: ['ball', 'flying-disc'],
    getHobby: {
        length: 2
    }
}
animal.hobby.push("run")
console.log(animal.getHobby.length);// 2
console.log(animal.hobby.length); // 3

递归类型别名

当我们在写对象的接口与类型别名时可能会遇到树形结构或者嵌套对象的情况,如原型链,菜单的子项,此时我们需要一种递归结构,允许我们在类型中调用自己,如

type MenuItem = {
    label: string
    key: string
    url: string
}
type Menu<T> = {
    value: T
    children?: Menu<T>[]
}

const menu: Menu<MenuItem> = {
    value: {
        label: '菜单1',
        key: 'menu1',
        url: '/menu1'
    },
    children: [
        {
            value: {
                label: '子菜单1',
                key: 'child1',
                url: '/child1'
            },
            children: [

            ]
        }, {
            value: {
                label: '子菜单2',
                key: 'child2',
                url: '/child2'
            },
            children: [

            ]
        },
    ]
}

上面的代码中我们使用递归类型别名实现了一个简单的二级菜单

条件类型

在JS中,我们都使用过三元运算符:a ? b : c。在TS中也有这种写法,我们称其为条件类型,它可以根据类型参数的属性或其他类型信息选择类型的一部分,比如

type ReturnObject<T> = T extends { [key: string]: any } ? T : null
type isNotObj = ReturnObject<false>
type isObj = ReturnObject<{ name: "张三" }>

上面的代码我们实现了对象类型的约束,如果传入的类型是对象类型则返回该类型,否则返回null

分发条件类型

TS中的分发条件类型可以将复杂的类型转换分解成更小的类型,并最终将它们组合在一起

比如我们使用条件类型实现一个简单的类型检查器,如果是数字,字符或布尔类型就获取各自的字符串,否则返回other字符串

type IGetType<T> = T extends string ? 'str'
    : T extends number ? 'num'
    : T extends boolean ? 'bool'
    : 'other'
type INum = IGetType<number>// num
type IBool = IGetType<boolean>// bool
type IStr = IGetType<string>// str
type IOther = IGetType<unknown>// other

类型过滤

顾名思义类型过滤就是在一个集合中过滤出符合条件的类型

基于上面的概念,我们可以实现一个include的类型检查,下面的函数中我们实现了一个类型过滤,只允许字符、数字、布尔类型通过

type MyInclude<T, U> = U extends T ? U : never;
type whiteList = string | number | boolean // 允许的类型 
type getString = MyInclude<whiteList, string> // string
type getArray = MyInclude<whiteList, string[]> // never

类型推导

infer关键字

在了解类型推导前,我们要先熟悉一下infer关键字,在2.8版本的TS中出现了这样一个MR

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : T;

上述代码中的infer R表示什么?我们先看看使用场景

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : T;
type IGetStr = MyReturnType<() => string> // string
type IGetNum = MyReturnType<() => number> // number
type IStr = MyReturnType<string>// string

明白了吗?上面代码中的infer将函数的返回值提取成R,当我们传入一个函数类型时,MyReturnType类型就会返回该函数的返回值,否则就返回原类型。

思考以下类型的实现

type MyArrayItem<T> = T extends (infer Item)[] ? Item : T;
type IStr = MyArrayItem<string>// string
type INumArr = MyArrayItem<number[]>// number

上面代码中我们实现了提取数组的子项类型的功能

回到类型推导

实际上类型推导就是根据函数参数的类型,推导出函数返回值的类型,我们借助上面的MyReturnType对函数的返回值类型进行推导

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : T;
const concatStr = (str1: string, str2: string) => {
    return str1 + str2
}
const addNum = (num1: number, num2: number) => {
    return num1 + num2
}
type concatStrReturn = MyReturnType<typeof concatStr> // string
type concatNumReturn = MyReturnType<typeof addNum> // number

映射&索引类型

之前的文章中,我们介绍了keyof和in关键字,并且使用映射与索引类型对对象类型进行了复制,并将属性值设置成了string

type IAnimal = {
    name: string
    age: number
    hobby: string[]
}
type IDog = {
    [key in keyof IAnimal]: string
}

索引访问类型

在JavaScript中,我们使用对象的索引取对象属性:Object.key或者Object[‘key’],取对象中的key属性,而在TypeScript中我们可以通过Object[key]来取对象类型的key属性

于是,我们可以写一个获取对象属性的类型

interface IAnimal {
    name: string
    age: number
    hobby: string[]
}
type GetItem<T, K extends keyof T> = T[K]
type AnimalName = GetItem<IAnimal, 'name'>// string
type AnimalAge = GetItem<IAnimal, 'age'>// number

映射类型

基于上述代码,我们可以进一步拓展,使用IAnimal[key]表示IAnimal的每一个属性,这个key可以理解成是一个对象属性名的集合(联合类型),即 name | age | hobby

type IDog = {
    [key in keyof IAnimal]: IAnimal[key]
}

上述代码表示的就是IAnimal的每一项

通过这种写法,我们可以将IAnimal提取成泛型,写一个通用的类型别名函数,达到遍历对象每一个属性并设置成只读的目的

type ReadonlyObject<T> = { readonly [key in keyof T]: T[key] };

我们来试用一下

type IAnimalReadonly = ReadonlyObject<IAnimal>
/*
等同于
type IAnimalReadonly = {
    readonly name: string;
    readonly age: number;
    readonly hobby: string[];
}
*/

必选属性

在TS的映射类型中,有许多可选属性,我们要如何批量改成必选属性呢?

这个时候我们可以给属性名添加’-?’ 符号达到该目的,如

type IAnimal = {
    name?: string
    age?: number
    hobby?: string[]
}
type IDog = {
    [key in keyof IAnimal]-?: string
}

此时的IDog的每一项就都是必选属性了

可变属性

与只读属性readonly对应的是可变属性,和上述必选属性类似通过在属性名前加-readonly 来实现此效果

type IAnimal = {
   readonly name: string
   readonly age: number
   readonly hobby: string[]
}
type Mutable<T> = {
   -readonly [key in keyof T]: string
}
type IAni = Mutable<IAnimal>

结语

以上就是文章所有内容,本文针对泛型的进阶使用,主要讲述了泛型的约束、递归类型别名、条件类型、映射和索引类型。以及它们的详细用法

感谢你看到最后,如果文章对你有帮助还请支持一下!

相关文章

索引签名 | 深入理解 TypeScript

泛型 | 深入理解 TypeScript

infer | 深入理解 TypeScript