TypeScript(十)泛型进阶

简介: TypeScript(十)泛型进阶

前言

本文收录于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<string>,简单的说就是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>

结语

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

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

相关文章
|
7月前
|
JavaScript 前端开发 编译器
TypeScript【泛型1、泛型2、声明合并、命名空间 、模块1、模块2、声明文件简介】(五)-全面详解(学习总结---从入门到深化)
TypeScript【泛型1、泛型2、声明合并、命名空间 、模块1、模块2、声明文件简介】(五)-全面详解(学习总结---从入门到深化)
135 0
|
7月前
|
JavaScript 编译器
TypeScript中泛型在函数和类中的应用
【4月更文挑战第23天】TypeScript的泛型在函数和类中提供了灵活性,允许处理多种数据类型。了解泛型是掌握TypeScript类型系统的关键。
|
3月前
|
JavaScript 编译器
typescript之泛型
typescript之泛型
135 60
|
2月前
|
JavaScript 前端开发
TypeScript【类型别名、泛型】超简洁教程!再也不用看臭又长的TypeScript文档了!
【10月更文挑战第11天】TypeScript【类型别名、泛型】超简洁教程!再也不用看臭又长的TypeScript文档了!
|
3月前
|
JavaScript 安全
typeScript进阶(14)_泛型和注意事项
TypeScript中的泛型允许创建可重用的代码。泛型可以定义函数、接口、类,支持传递类型参数,实现类型安全。泛型可以用于数组,约束类型参数必须符合特定的接口,也可以在接口和类中使用。泛型类可以包含多个类型参数,甚至在泛型约束中使用类型参数。
28 1
typeScript进阶(14)_泛型和注意事项
|
2月前
|
JavaScript 安全 前端开发
TypeScript :枚举&字符&泛型
本文介绍了 TypeScript 中的泛型、约束、枚举和字符操作的基本用法。通过示例代码展示了如何定义和使用泛型函数、类和接口,以及如何利用 `keyof` 约束类型。此外,还介绍了枚举的定义和使用,包括常量枚举和外部枚举的区别。最后,简要说明了 `?.` 和 `??` 操作符的用途,帮助处理可能为空的属性和提供默认值。
|
4月前
|
JavaScript 安全 算法
TypeScript:一个好泛型的价值
TypeScript:一个好泛型的价值
|
5月前
|
JavaScript 前端开发 程序员
Typescript 【实用教程】(2024最新版)含类型声明,类型断言,函数,接口,泛型等
Typescript 【实用教程】(2024最新版)含类型声明,类型断言,函数,接口,泛型等
88 0
|
6月前
|
JavaScript
TypeScript 泛型类型
TypeScript 泛型类型
|
6月前
|
JavaScript Java 编译器
TypeScript 泛型
TypeScript 泛型