进阶学习TypeScript之类型体操-基础篇

进阶学习TypeScript之类型体操-基础篇

类型体操是一种调侃的说法,其来源于TypeScript中各种复杂的类型操作。
    Hello,大家好,我是BalladJz。近期经常在社区和社群看见很多小伙伴对TypeScript的各种讨论,褒贬不一,我还主动去找了关于TS的话题,看了一些为TS"正名"的文章,再看了评论里的唇枪舌战,真是为博主捏了一把汗,但本文不对TS的'好与不好'做任何评论,但我只能表达我自己是一个TS的重度爱好者

Okay~,回归正文吧,在众多的前端开发开发框架中都能碰见TypeScript身影,react+ts、vue+ts、three.js+ts等,就拿国内近年最火的Vue来说,Vue3也是利用TS编写的,且随着今年底的Vue2停止维护,Vue3的使用频率定然会逐步升高,而利用TS做类型校验的Vue3,TS的大概率会有更多的前端小伙伴使用(这里插入一个额外的话题,有小伙伴知道在Vue2是怎样做类型校验的吗?-- 这里不赘述了 感兴趣的小伙伴可以了解一下flow),而现在还有很多人比较排斥TS

但是!同志,请相信我,认真对待写TS这件事,你必将有意外的收获

(什么TS有更好的开发体验、提示友好、类型错误校验、这些功能就不说了,这是最基本的,说说不常见的)

  1. 以上图片来自于我随手截的Vue仓库的一段代码。这种代码在基于TS的库中数不胜数,如果你熟悉ts工具类型,看源码定义不再吃力了,从源码的角度理解每一个工具类型的实现机制
  2. 不再为大佬写的 ts 定义而苦恼了
  3. 能够在平时的工作中举一反三,规避低级错误,比如函数传参错误、边界情况的判断
  4. 复杂的大型项目,能够保证数据结构层级不错位,更好的抽象能力,部分枚举、模型和后端保持一致

给大家看一下我曾经面试遇到的两道关于TS类型编程的题 实话讲,作为菜鸡的我是没有做出来的,但它确实难度不高,有趣的是,面试官直接说: "这两道题实际工作中完全用不到,我就想看看你能不能写出来"(那心里叫一个苦啊),当然题目的答案会在本文最后揭晓。

本文将会通过18个类型工具,基本大致是照着官方的文档读下来的(第11题和最后一题面试题答案),记录下来也方便以后自己回顾,会讲解TS类型工具原理和编程技巧。不扯闲篇,全程干货,内容非常多,想提升Ts功力的小伙伴请耐心读下去。相信小伙伴们在读完此文后,能够对这块有更深入的理解。下面,我们开始吧~

当然,本文需要有TS的一些基本功:比如索引访问、映射类型、条件链、keyof、infer、extends(条件类型、继承/拓展、分配的含义)等

1.MyAwaited

Awaited<Type> 对函数await中的async操作或 s.then()上的方法进行建模,根据Promise对象 返回它的类型,在TS中使用Promise中的T来描述这个Promise返回的类型

ts 复制代码
/**
 * desc: 首先利用extends条件判断,是否可以推断(infer)出 Promise的 T类型,如果可以,我们返回它的类型,反之 我们直接返回T
 *           (以为这就完了吗?显然肯定不是这样的,别忘了await、async出现的原因,它们是为了解决Promise嵌套地域的问题,所以此处我们还需要对多层Promise<T>的处理)
 *       也很简单,简单的说就是对这个工具类进行递归,
 *       当我们通过 T extends Promise<infer R> 推断出 R的类型后,再次(使用T extends Promise<infer R>)对R进行条件判断
 *           如果再次推断出来了R的类型 那证明这个T就是一个嵌套的Promise类型,不管嵌套多少层,依次去进行判断就行
 *           当到最后一层时,它定会走 false 分支,最后直接返回我们需要的类型
 */
 
type MyAwaited<T> = T extends Promise<infer R>
    ? R extends Promise<any> ? MyAwaited<R> : R
    : T
ts 复制代码
type T1 = MyAwaited<Promise<Promise<Promise<string>>> | boolean> // string | boolean

2.MyPartial

MyPartial<Type> 将所有类型设置为可选

ts 复制代码
/** 
 * desc: 传入一个泛型参数,主要通过K extends keyof T约束K必须为T的子类型,
 *       再根据 索引访问 该键的类型,最终keyof T得到的是T的所有key组成的联合类型
 */
 
type MyPartial<T> = {
    [P in keyof T]?: T[P]
}
ts 复制代码
// 准备一个User类型
type User = {
    name: string
    age: number
    isAdult: boolean
    children: User
    desc: () => any
}

type T2 = MyPartial<User> // 最终的类型是将User的所有类型转换为可选类型

3.MyRequired

MyRequired<Type> 将所有类型设置为必选

ts 复制代码
/** 
 * desc: MyRequired 与 MyPartial完全相反 只是在最后利用 映射修饰符 去掉可选条件
 */
 
type MyRequired<T> = {
    [P in keyof T]?: T[P]
}
ts 复制代码
// 这里的T2的类型经过MyPartial的转换他的所有属性都是可选的  
// type T2 = {
//     name?: string | undefined;
//     age?: number | undefined;
//     isAdult?: boolean | undefined;
//     children?: any | undefined;
//     desc?: (() => void) | undefined;
// }

// 再使用 MyRequired 进行转换一次
type T3 = MyRequired<T2> // 最终的类型是将User的所有类型转换为必选类型

4.MyReadonly

MyReadonly<Type> 将所有属性Type设置为 仅读属性(不可修改)

ts 复制代码
/** 
 * desc: 主要实现是通过映射遍历所有key,然后给每个key增加一个readonly修饰符
 */
 
type MyReadonly<T> = {
    readonly [K in keyof T]: T[K]
}
ts 复制代码
type T4 = MyReadonly<User>

5.MyRecord

MyRecord<Keys, Type> 将其属性键为Keys,属性值为Type。该实用程序可用于将一种类型的属性映射到另一种类型。

ts 复制代码
/** 
 * desc: 须对第一个泛型参数中做类型约束,只支持 string | number | symbol
 */
 
type MyRecord<T extends string | number | symbol, P> = {
    [K in T]: P
}
ts 复制代码
type T5 = MyRecord<string, number> //  { [x: string]: number; }

6.MyExclude

MyExclude<UnionType, ExcludedMembers> UnionType 排除所有可分配给的联合成员来构造类型 ExcludedMembers

ts 复制代码
/** 
 * desc: 利用条件分配类型,如果前一个泛型参数的类型,在第二个参数中存在 就返回never
 *       如果不存在,就返回 T,最终构造出新的类型
 */
 
type MyExclude<T, P> = T extends P ? never : T
ts 复制代码
type ExA = '1' | 2 | { name: string } | symbol | ((...arg: any) => void)
type ExB = '1' | symbol
type T6 = MyExclude<ExA, ExB> // { name: string } | 2 | ((...arg: any) => void)

7.MyExtract

MyExtract<Type, Union> Type 通过从可分配给的所有联合成员中提取来构造类型Union。

ts 复制代码
/** 
 * desc: 与MyExclude完全相反:如果前一个泛型参数中的类型,在第二个泛型参数中存在 就返回T,
 *        如果不存在,就返回 never,最终构造出新的类型
 */
 
type MyExtract<T, P> = T extends P ? T : never
ts 复制代码
type ExC = '1' | 2 | { name: string } | symbol | ((...arg: any) => void)
type ExD = '1' | Function
type T7 = MyExtract<ExC, ExD> // "1" | ((...arg: any) => void)

8. MyPick

MyPick<Type, Keys> 从 Type 中取出 Key 的属性集

ts 复制代码
/** 
 * desc:在泛型参数上须对第二个泛型参数中做类型约束,利用 extends 与 keyof 进行约束和获取第一个泛型的键即可
 *          然后根据第二个泛型参数中的类型,从第一个泛型参数中 取出出符合第二个泛型参数的新类型
 */
 
type MyPick<T, K extends keyof T> = {
    [P in K]: T[P]
}
ts 复制代码
type T8 = MyPick<User, 'name' | 'desc' | 'children'> // { name: string; desc: () => void; children: User; }

9. MyOmit

MyOmit<Type, Keys> 从 Type 中删除 Key 的属性集(与MyPick相反)

ts 复制代码
/** 
 * desc:根据第二个泛型参数中的类型,从第一个泛型参数中 排除出符合第二个泛型参数,而构造出的新类型
 *      需要注意的是,要排除第一个泛型的参数,需要用到上面的 MyExclude 工具类,从而获取到新的类型
 */
 
type MyOmit<T, K extends string | number | symbol> = {
    [P in MyExclude<keyof T, K>]: T[P]
}
ts 复制代码
type T9 = MyOmit<User, 'name' | 'age'> // { desc: () => void; children: User; isAdult: boolean; }

值得注意的是:

ts 复制代码
/** 
 * 值得注意的是:
 * MyPick与MyExtract 和 MyExcludes与MyOmit的区别
 * 
 * MyPick与MyExtract:
 *     可以大致理解为,它们都是从取出第一个泛型参数中取出符合第二个泛型参数的键值,且它们的第二个参数都是联合类型
 *     但MyPick的第一个参数是一个Object类型,第二个参数是利用 keyof 获取到的Object的键的联合类型
 *     MyExtract前后两个参数都更适用于联合类型  type T06 = MyExclude<{ name: string, age: number } | never, string> // type T06 = { name: string; age: number; }
 * 
 * MyExclude与MyOmit: 与上面的MyPick与MyExtract 去反向理解即可
 */

10. MyNonNullable

MyNonNullable<Type> 排除 null undefined 构造类型Type

ts 复制代码
/** 
 * desc 1.可以根据对T进行约束为 null | undefined 如果 条件判断为真 则返回never 否则 返回我们需要的类型, 
 *      2.通过上面的阅读,这个不就是对 MyExclude 进行使用吗? 对MyExclude的第二个参数传入null和undefined的联合类型即可
 *      3.但实际上还有更简单的方式,利用 & 
 */
 
type MyNonNullable<T> = T extends null | undefined ? never : T
type MyNonNullable1<T> = MyExclude<T, null | undefined>
type MyNonNullable2<T> = T & {}

type T10 = MyNonNullable<string | Array<number> | undefined> // string | number[]
type T11 = MyNonNullable1<string | Array<number> | undefined> // string | number[]
type T12 = MyNonNullable2<string | Array<number> | undefined> // string | number[]

11. MyReturnType

MyReturnType<Type> 获取函数的返回值类型

ts 复制代码
/** 
 * desc 此工具类需要对Type进行约束为一个Function类型,对其返回值使用infer关键字进行类型推断,且在推断时 需要使用typeof去获取到Function的类型
 *      如果可以可以推断的话,那就返回新的类型,反之 返回never,最终根据构造成一个新的联合类型
 */
 
type MyReturnType<T extends (...arg: any) => any> = T extends (...arg: any) => infer R ? R : never
ts 复制代码
// 此处准备两个函数
const fn1 = (a: boolean, b: number, c: string): number | string | void => { }
const fn2 = () => {
    return { name: 'zs', age: 18 }
}

type T13 = MyReturnType<typeof fn1> // string | number | void
type T14 = MyReturnType<typeof fn2> // { name: string, age: number }

12. MyParameters

MyParameters<Type> 获取函数的参数类型 并构成一个新的元组类型

ts 复制代码
/** 
 * desc MyParameters与MyReturnType很相似,也需要对Type进行约束为一个Function类型,而在推断时则不是推断返回值了,而是推断参数
 *      如果可以推断的话,那就返回新的类型,反之 返回never,最终根据构造成一个参数类型元组
 */

type MyParameters<T extends (...arg: any) => any> = T extends (...arg: infer R) => any ? R : never
ts 复制代码
const fn1 = (a: boolean, b: number, c: string): number | string | void => { }
const fn2 = () => {
    return { name: 'zs', age: 18 }
}

type T15 = MyParameters<typeof fn1> // [a: boolean, b: number, c: string]
type T16 = MyParameters<typeof fn2> // []

13. MyConstructorParameters

MyConstructorParameters<Type> 可以获取类的构造函数的参数类型,存在一个元组中

ts 复制代码
/**
 * desc: 此方法与MyParameters 很相似,只是在此处要对T约束为 abstract 抽象类,通过infer 推断出类的constructor参数类型,并得到一个元组
 *     此处有两个重点 ==> 请看下面说明
 */ 
class Person {
    constructor(name: string, age: number, isAdult: boolean) {
        this.name = name;
        this.isAdult = isAdult;
    }
    name: string = 'zs';
    isAdult: boolean = false;
}
type MyConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer R) => any
    ? R
    : never
ts 复制代码
type T17 = MyConstructorParameters<typeof Person> // [name: string, age: number, isAdult: boolean]
type T18 = MyConstructorParameters<any> // unknown[]

14. MyInstanceType

MyInstanceType<Type> 可以获取类的构造函数的实例的类型,组成一个新的类型Type。

ts 复制代码
/**
 * desc: 反向理解MyConstructorParameters 无非是使用infer R的位置不一样  
 *       通过infer 推断构造函数的实例 即可
 */
type MyInstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any
ts 复制代码
type T19 = InstanceType<typeof Person> //  { name: string, isAdult: boolean }

const BalladJz: T19 = {
    name: 'ballad_jz',
    isAdult: true
}

说明

ts 复制代码
    class OrdinaryClass { }
    abstract class AbstractClass { }

    const c1: typeof OrdinaryClass = OrdinaryClass // 可以赋值
    const c2: typeof OrdinaryClass = AbstractClass // 不能赋值 => 无法将抽象构造函数类型分配给非抽象构造函数类型。

    const c3: typeof AbstractClass = OrdinaryClass // 可以赋值
    const c4: typeof AbstractClass = AbstractClass // 可以赋值
    /**
     * 由此看出,如果将类型定义为抽象类(抽象构造函数),则既可以赋值为抽象类,也可以赋值为普通类;而反之则不行。
     */

    /**
     * 定义一个类
     */
    class People {
        name: string = 'ww';
        age: number = 18;
        constructor() { }
    }

    const p1: People = new People(); // p1可以正常赋值
    const p2: People = People; // 等号后面的People报错,类型"typeof People"缺少类型"People"中的以下属性: name, age

    const p3: typeof People = new People(); // p3报错,类型 "People" 中缺少属性 "prototype",但类型 "typeof People" 中需要该属性
    const p4: typeof People = People; // p4可以正常赋值

    /**
     * 由此看出 
     *  当把类直接作为类型时,该类型约束的是该类型必须是类的实例;即该类型获取的是该类上的实例属性和实例方法(也叫原型方法)
     *  当把typeof 类作为类型时,约束的满足该类的类型;即该类型获取的是该类上的静态属性和方法。
     */

15. MyThisParameterType

MyThisParameterType<Type> 提取函数类型的this参数的类型,如果函数类型没有参数,则提取未知this。

ts 复制代码
/**
 * desc: 此处需要利用infer进行类型推断,如果可以推断出this的类型,则直接返回this的类型,否则返回 unknown
 */
type MyThisParameterType<T> = T extends (this: infer R, ...args: any) => any ? R : unknown
ts 复制代码
// 定义一个函数
function toFunThisType(this: { time: number; address: string }) {
    return this
}

type T20 = MyThisParameterType<typeof toFunThisType> // { time: number; address: string; }

16. MyOmitThisParameter

MyOmitThisParameter<Type> 剔除 this 参数 MyThisParameterType

ts 复制代码
// 定义三个函数
function toWipeFunctionThisType_1(this: string, a: number) {
    return this + a
}
function toWipeFunctionThisType_2(a: string) {
    return a
}
function toWipeFunctionThisType_3() { }
/**
 * desc:  如果传递的函数不包含this参数,则直接返回函数的类型;如果包含this参数,则剔除this参数 返回不带this类型的函数的类型
 *        MyOmitThisParameter 需要结合到 MyThisParameterType 进行条件判断, (如何判断是否存在this类型 请看下图)
 *           ① 如果 MyThisParameterType 推断出来了是 unknown,那就证明这个函数没有this的类型,我们直接返回 typeof T 函数的类型
 *           ② 反之,就证明这个函数存在this类型 则我们直接执行 T extends (...args: infer A) => infer R ? (...args: A) => R : T
 *                  再使用infer 推断出参数的类型和返回值的类型,如果可以推断出来就返回 剔除函数参数this类型后的类型,否则就返回 T
 * 
 */

/** 定义一个判断函数参数是否有this类型,如果没有函数的类型,如果有就返回 '有this类型'字面量  */
type IsExistThisType<T> = unknown extends ThisParameterType<T> ? T : '有this类型'
type N1 = IsExistThisType<typeof toWipeFunctionThisType_1> // '有this类型'             --> toWipeFunctionThisType_1 他是有this类型的
type N2 = IsExistThisType<typeof toWipeFunctionThisType_2> // (a: string) => string   --> toWipeFunctionThisType_2 是没有this类型的 通过typeof直接返回的函数类型
type N3 = IsExistThisType<typeof toWipeFunctionThisType_3> // () => void              --> toWipeFunctionThisType_3 也是没有this类型的 通过typeof直接返回的函数类型


// 根据上面的案例,糅合一下,就得到了我们的 MyOmitThisParameter
type MyOmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T
ts 复制代码
type T21 = MyOmitThisParameter<typeof toWipeFunctionThisType_1> //  (a: number) => string
type T22 = MyOmitThisParameter<typeof toWipeFunctionThisType_2> //  (a: string) => string
type T23 = MyOmitThisParameter<typeof toWipeFunctionThisType_3> //  () => void

17. ThisType

ThisType<Type> 此实用工具不返回转换后的类型。相反,它充当上下文 this 类型的标记。

ts 复制代码
// 比如 有一个对象类型 MyObject ,它有一个属性 name 和一个方法 getName ,该方法返回对象的 name 属性值:
type MyObject = {
    name: string;
    getName(): string;
};

// 我们可以再创建一个新的对象类型 MyNewObject ,它与 MyObject 类型相同,但在 getName 方法中使用 this 关键字来引用对象本身。
type MyNewObject = ThisType<{ name: string }> & {
    getName(this: MyNewObject): string;
};

// 我们可以使用 ThisType<Type> 工具类型来实现这个目标
type MyNewObject_1 = {
    name: string;
    getName(this: MyNewObject_1): string;
}


// 可以看到, MyNewObject 类型与 MyObject 类型相同,只是在 getName 方法中使用了 this 关键字来引用对象本身。这是通过使用 ThisType<Type> 工具类型来实现的,其中 Type 参数指定了 this 关键字的类型。

18. MyIsIncludes

ts 复制代码
/** 
 * question:MyIsIncludes 在类型系统里面实现 js中 Array.includes 方法,这个类型接受两个参数,如果第1个参数包含了第2个参数,那么返回true,否则返回false
 * 
 * answer: 1.此工具类需要对泛型做一个约束,先通过 extends 约束T为一个Array类型
 *         2.再根据 extends 分配出第一个参数的第一项和剩余项,通过infer推断,
 *         3.如果不能推断出来直接返回 false
 *         4.如果可以推断出来,再利用 extends 进行第一个参数的第一项和第二个参数的判断是否是同一类型
 *              如果是同一个类型,那我们直接返回 true
 *              如果不是同一类型,则继续递归 MyIsIncludes,直至把第一个参数的所有项与第二个参数判断完成,仍没有的话 那就返回false
 *         over~ 
 *
 *  如下:
 */

type MyIsIncludes<T extends Array<unknown>, U> = T extends [infer F, ...infer R] ? (F extends U ? true : MyIsIncludes<R, U>) : false
type T24 = MyIsIncludes<['1', '2'], '3'>  // false
type T25 = MyIsIncludes<['1', '2'], '2'>  // true

以为这就完了吗?上面的 MyIsIncludes是可以推出大部分类型,但在类型系统里,是分层级的

比如:比如顶层的any unknown,中间的string, number, 底层的never,他们之间的类型全都判断还是存在一定问题的

ts 复制代码
// 不完善的工具:MyIsIncludes   此处的 F extends U 并不能完全判断出 类型相等 (此处的判断是全等判断)
// 比如:
type T26 = MyIsIncludes<[string, 2], any>  // true
type T27 = MyIsIncludes<[true, 2], boolean>  // true
// T26 T27的类型都是 true (这显然不是想要的结果)
// 当然这个不只是string和any、true/false与boolean有这个问题,也包括readonly、可选属性等

优化后

ts 复制代码
/**
 * 只需要对(基于F extends U)这块儿判断进行优化一下即可
 *   它的基本骨架是type IfEquals<> = (参数1) extends (参数2) ? A : B (这里的A B没有实际意义,随便给个字符串和数字都行,只是利于我们判断类型是否全等)
 *   如果第一个参数能够分配给第二个参数那就返回A 如果 不能那就返回B,
 *   最终我们再判断 A extends B 就完成了  F extends U 的优化
 * 
 */
type IsEqual<T, U> = (<G>() => G extends T ? '1' : '2') extends (<G>() => G extends U ? '1' : '2') ? true : false

type CheckIsEqual_1 = IsEqual<true, boolean> // false
type CheckIsEqual_2 = IsEqual<{ name: string }, { readonly name: string }> // false
type CheckIsEqual_3 = IsEqual<string, string> // true

// 再把我们的 IsEqual 替换上面的 F extends U 

type MyIsIncludes<T extends Array<unknown>, U> = T extends [infer F, ...infer R]
    ? (IsEqual<F, U> extends true ? true : MyIsIncludes<R, U>)
    : false

type T28 = MyIsIncludes<[string, 2], any>  // false
type T29 = MyIsIncludes<[true, 2], boolean>  // false
type T30 = MyIsIncludes<[1, 2], 1> // true
type T31 = MyIsIncludes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // false
type T32 = MyIsIncludes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Kars'> // true

写在最后

如果大家对TS感兴趣可以一起去做一下 type-challenges 里面两百多道关于TS类型的题

参考内容:www.typescriptlang.org/docs/handbo...

相关推荐
王解1 天前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
鸿蒙开天组●1 天前
鸿蒙进阶篇-网格布局 Grid/GridItem(二)
前端·华为·typescript·harmonyos·grid·mate70
zhizhiqiuya1 天前
第二章 TypeScript 函数详解
前端·javascript·typescript
初遇你时动了情2 天前
react 18 react-router-dom V6 路由传参的几种方式
react.js·typescript·react-router
王解2 天前
Jest进阶知识:深入测试 React Hooks-确保自定义逻辑的可靠性
前端·javascript·react.js·typescript·单元测试·前端框架
_jiang2 天前
nestjs 入门实战最强篇
redis·typescript·nestjs
清清ww2 天前
【TS】九天学会TS语法---计划篇
前端·typescript
努力变厉害的小超超3 天前
TypeScript中的类型注解、Interface接口、泛型
javascript·typescript
王解3 天前
Jest进阶知识:整合 TypeScript - 提升单元测试的类型安全与可靠性
前端·javascript·typescript·单元测试
Vesper634 天前
【TS】TypeScript 类型定义之联合类型(union types)和交叉类型(intersection types)
linux·ubuntu·typescript