TypeScript泛型的高级用法:第二部分

泛型在开发组件或库时非常有用

在本文中,我将介绍如何使用TypeScript泛型来声明 VueBasicProps 函数来完成以下挑战。在挑战中,我还会介绍一些非常有用的TypeScript知识。掌握了以后,应该会对你的工作有所帮助。
 

挑战
 

在“TypeScript泛型的高级用法第1部分”一文中,我们声明了 SimpleVue 函数。接下来,我将扩展这个函数的功能,向原来的 options 形参对象添加一个新的 props 属性。这是Vue  props 选项的简化版本。这里有一些规则。

  • props 是一个对象,该对象的每个属性都会被注入到 this 上下文中。注入的 props 属性可以在所有上下文中访问,包括 data 、 computed 和 methods 。

  • 单个 prop 属性可以通过构造函数或具有 type 属性的对象来定义。

例如:

props: {  foo: Boolean}// orprops: {  foo: { type: Boolean }}

上述道具将被推断为 type Props = { foo: boolean } 。当传递多个构造函数时,应该将属性的类型推断为联合类型。

props: {  foo: { type: [Boolean, Number, String] }}// -->type Props = { foo: boolean | number | string }

如果当前属性的类型为空对象类型( {} ),则该属性的类型将被推断为类型 any 。

 VueBasicProps 函数的使用示例如下:

class ClassA {}VueBasicProps({  props: {    propA: {},    propB: { type: String },    propC: { type: Boolean },    propD: { type: ClassA },    propE: { type: [String, Number] },    propF: RegExp,  },  data(this) {    type PropsType = Debug<typeof this>    type cases = [      Expect<IsAny<PropsType['propA']>>,      Expect<Equal<PropsType['propB'], string>>,      Expect<Equal<PropsType['propC'], boolean>>,      Expect<Equal<PropsType['propD'], ClassA>>,      Expect<Equal<PropsType['propE'], string | number>>,      Expect<Equal<PropsType['propF'], RegExp>>,    ]    // @ts-expect-error    this.firstname    // @ts-expect-error    this.getRandom()    // @ts-expect-error    this.data()    return {      firstname: 'Type',      lastname: 'Challenges',      amount: 10,    }  },  computed: {    fullname() {      return `${this.firstname} ${this.lastname}`    },  },  methods: {    getRandom() {      return Math.random()    },    hi() {      alert(this.fullname.toLowerCase())      alert(this.getRandom())    },    test() {      const fullname = this.fullname      const propE = this.propE      type cases = [        Expect<Equal<typeof fullname, string>>,        Expect<Equal<typeof propE, string | number>>,      ]    },  },})

在上面的例子中,使用了TypeScript 3.9中引入的一个新特性——// @ts-expect-error注释。把它放在代码前面,TypeScript就会忽略这个错误。如果代码中没有错误,TypeScript编译器会指出代码中有一个没有使用的指令(@ts-expect-error)。

另外,本例中还使用了 Expect 、 Equal 和 IsAny 实用程序类型,相关代码如下:

type Expect<T extends true> = Ttype Equal<X, Y> =  (<T>() => T extends X ? 1 : 2) extends  (<T>() => T extends Y ? 1 : 2) ? true : falsetype IsAny<T> = 0 extends (1 & T) ? true : falsetype Debug<T> = { [K in keyof T]: T[K] }


解决方案
 

由于 VueBasicProps 函数为原 SimpleVue 函数的 options 形参对象添加了一个新的 props 属性,因此我们引入了一个新的类型形参 Props 来表示其类型,并使用 extends 关键字来约束该类型形参的类型。

declare function VueBasicProps<  Data extends Record<string, any>,  Computed extends Record<PropertyKey, (...args: unknown[]) => unknown>,  Methods extends Record<PropertyKey, (...args: unknown[]) => unknown>,  Props extends Record<string, any>>(options: {    data: (this: void) => Data,    computed: Computed & ThisType<Data>,    methods: Methods & ThisType<    & Data    & Methods    & {        [P in keyof Computed]: ReturnType<Computed[P]>    }>,    props: Props}): any

在类型挑战描述中,要求注入的 props 属性在所有上下文中都是可访问的,包括 data 、 computed 和 methods 。为了访问 data 函数中 props 对象的属性,我们需要修改 data 函数的类型签名。也就是说,我们需要将 this: void 改为 this: Props 。之后,在示例中的 data 函数中定义的 PropsType 类型将生成以下类型:

type PropsType = {    propA: {};    propB: {      type: StringConstructor;    };    propC: {      type: BooleanConstructor;    };    propD: {      type: typeof ClassA;    };    propE: {      type: (StringConstructor | NumberConstructor)[];    };    propF: RegExpConstructor;}

显然,此时的 PropsType 类型不能满足以下所有测试用例:

type cases = [  Expect<IsAny<PropsType["propA"]>>, // ❌  Expect<Equal<PropsType["propB"], string>>, // ❌  Expect<Equal<PropsType["propC"], boolean>>, // ❌  Expect<Equal<PropsType["propD"], ClassA>>, // ❌  Expect<Equal<PropsType["propE"], string | number>>, // ❌  Expect<Equal<PropsType["propF"], RegExp>> // ❌]

为了满足上述测试用例,我们需要修改 data 函数中 this “伪参数”的类型。因此,我们需要定义一个 PropsTypes 泛型来执行类型转换,但您也可以使用其他名称。

type PropsTypes<T> = {  [P in keyof T]: T[P]}

在前面的PropsType 类型中,我们看到了 StringConstructor 、 BooleanConstructor 和 NumberConstructor 类型,它们是在TypeScript模块中的lib/lib.es5.d.ts文件中定义的:

// lib/lib.es5.d.tsinterface StringConstructor {    new(value?: any): String; // construct signature    (value?: any): string; // call signature    readonly prototype: String;    fromCharCode(...codes: number[]): string;}interface BooleanConstructor {    new(value?: any): Boolean; // construct signature    <T>(value?: T): boolean; // call signature    readonly prototype: Boolean;} interface NumberConstructor {    new(value?: any): Number; // construct signature    (value?: any): number; // call signature    readonly prototype: Number;    // omit other properties}interface RegExpConstructor {    new(pattern: RegExp | string): RegExp;    new(pattern: string, flags?: string): RegExp;    (pattern: RegExp | string): RegExp;    (pattern: string, flags?: string): RegExp;    readonly prototype: RegExp;    // omit other properties}

从上面的代码可以看出,StringConstructorBooleanConstructor 和 NumberConstructor 类型呼叫签名的返回类型是与该类型对应的基本数据类型。那么我们如何在上述类型中提取调用签名的返回值类型呢?这就是我们需要使用条件类型和 infer 类型推断的地方,如下面的代码所示:

type ResultType<T> =   T extends { (value?: any): infer R }   ? R : anytype R0 = ResultType<StringConstructor> // stringtype R1 = ResultType<BooleanConstructor> // booleantype R2 = ResultType<NumberConstructor> // numbertype R3 = ResultType<RegExpConstructor> // RegExptype R4 = ResultType<typeof ClassA> // any

在上面的代码中,我们定义了一个新的 ResultType 泛型。这个泛型已经正确地提取了 *Constructor 类型中调用签名的返回类型。但是 typeof ClassA 构造函数类型的返回类型还不能正确提取。

要解决这个问题,我们需要运用施工签名的知识。如果您不了解调用签名和构造签名,可以阅读下面的文章(注意,直接点击会报错,需要用右键复制链接地址到浏览器地址中打开)。

关于TypeScript Interface你需要知道的10件事TypeScript接口的10种使用场景——可能只有20%的web开发人员完全掌握它们icon-default.png?t=N7T8https://mp.weixin.qq.com/s?__biz=MzU3NjM0NjY0OQ==&mid=2247484948&idx=1&sn=0cca3d731c2157286ca018938b9a16c8&chksm=fd140962ca638074085b7715cbf3d460c6d0579cd7c9c47ce5b5fab6e26e2571cd32199cc1ba&token=1779636375&lang=zh_CN#rd

type ResultType<T> =  T extends { (value?: any): infer R }  ? R : T extends { new(...args: unknown[]): infer I }  ? I : anytype R0 = ResultType<StringConstructor> // stringtype R1 = ResultType<BooleanConstructor> // booleantype R2 = ResultType<NumberConstructor> // numbertype R3 = ResultType<RegExpConstructor> // RegExptype R4 = ResultType<typeof ClassA> // ClassA

从上面的结果中,我们可以看到我们的 ResultType 泛型已经可以正确地提取类型。接下来,让我们更新前面定义的 PropsTypes 泛型:

type PropsTypes<T> = {  [P in keyof T]: ResultType<T[P]>}

使用 PropsTypes 泛型,我们可以再次修改 data 函数中 this “伪形参”的类型:

修改 this “伪参数”类型后,已通过2个测试用例。

type cases = [  Expect<IsAny<PropsType["propA"]>>, // ✅  Expect<Equal<PropsType["propB"], string>>, // ❌  Expect<Equal<PropsType["propC"], boolean>>, // ❌  Expect<Equal<PropsType["propD"], ClassA>>, // ❌  Expect<Equal<PropsType["propE"], string | number>>, // ❌  Expect<Equal<PropsType["propF"], RegExp>> // ✅]

为什么其他测试用例会失败?这是因为在这些失败的测试用例中使用的属性类型是由具有 type 属性的对象类型定义的,该属性指定当前 prop 的类型。

type PropsType = {    propA: {};    propB: {      type: StringConstructor;    };    propC: {      type: BooleanConstructor;    };    propD: {      type: typeof ClassA;    };    propE: {      type: (StringConstructor | NumberConstructor)[];    };    propF: RegExpConstructor;}

在上述属性中, propE 属性的特殊之处在于,该属性的类型是数组类型,并且数组中每一项的类型都是 StringConstructor | NumberConstructor 联合类型。要获取数组项的类型,可以使用索引访问类型:

type R5 = (StringConstructor | NumberConstructor)[][number]// StringConstructor | NumberConstructor

通过以上分析,我们可以看到,在更新 PropsTypes 泛型类型时,我们应该考虑对象类型和数组类型的情况。为了区分这两种类型,我们需要在TypeScript中使用条件类型,它们在下面的代码中实现:

type PropsTypes<T> = {  [P in keyof T]:    T[P] extends { type: infer R } // Get the prop type    ? R extends readonly unknown[] // Determine whether it is an array type      ? ResultType<R[number]> // Handle union types      : ResultType<R> // Handle a single type    : ResultType<T[P]>}

在更新 PropsTypes 泛型类型之后,示例中 data 函数中的所有测试用例都正确通过。然而, VueBasicProps 函数的类型声明还没有完成,因为 props 属性还不能通过 computed 和 methods 属性中的函数内部的 this 上下文来访问,所以我们需要改变 computed 和 methods 属性的类型。

computed: Computed & ThisType<Data & PropsTypes<Props>>,methods: Methods & ThisType<  & Data  & Methods  & PropsTypes<Props>  & {    [P in keyof Computed]: ReturnType<Computed[P]>  }>,

最后,让我们看一下完整的代码:

type PropsTypes<T> = {  [P in keyof T]:  T[P] extends { type: infer R }  ? R extends readonly unknown[]  ? ResultType<R[number]>  : ResultType<R>  : ResultType<T[P]>}type ResultType<T> =  T extends { (value?: any): infer R }  ? R : T extends { new(...args: unknown[]): infer I }  ? I : anydeclare function VueBasicProps<  Data extends Record<string, any>,  Computed extends Record<PropertyKey, (...args: unknown[]) => unknown>,  Methods extends Record<PropertyKey, (...args: unknown[]) => unknown>,  Props extends Record<string, any>>(options: {  data: (this: PropsTypes<Props>) => Data,  computed: Computed & ThisType<Data & PropsTypes<Props>>,  methods: Methods & ThisType<    & Data    & Methods    & PropsTypes<Props>    & {      [P in keyof Computed]: ReturnType<Computed[P]>    }>,  props: Props}): any

在本文中,除了TypeScript映射类型、条件类型和 infer 类型推断外,我们还使用调用签名和构造函数签名来完成 VueBasicProps 函数类型定义过程。

在阅读本文之后,我相信您已经知道如何确定类型是否是构造函数类型。关于 ThisType<Type> 内置泛型角色的更多信息,你可以阅读“TypeScript泛型的高级用法第1部分”文章。而 PropsTypes 和 ResultType 泛型在内部使用多个条件类型,也称为条件链,如果您感兴趣,可以阅读下面的文章(注意,直接点击会报错,需要用右键复制链接地址到浏览器地址中打开)。

像专家一样使用TypeScript映射类型掌握TypeScript的映射类型,了解TypeScript内置的实用类型是如何工作的。icon-default.png?t=N7T8https://mp.weixin.qq.com/s?__biz=MzU3NjM0NjY0OQ==&mid=2247484965&idx=1&sn=3e95787a17188b63769934fc42c5d72f&chksm=fd140953ca638045cf19f7915e04221ff2bc7d24aaa83f752b430e01d08c37baacf277e24876&token=1779636375&lang=zh_CN#rd 欢迎关注公众号:文本魔术,了解更多

  • 28
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值