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

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

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

挑战

提供一个 SimpleVue 函数(类似于  Vue.extend  或 defineComponent ),以正确推断  this  inside  computed  和  methods 的类型。

在这个挑战中,我们假设 SimpleVue 函数只接受具有 data 、 computed 和 methods 属性的对象作为其唯一参数:

  • data 是一个简单函数。调用此函数后,将返回一个对象并将其公开给 this 上下文。在 data 函数内部,您不能访问其他计算属性或方法。

  • computed 是一个属性值为函数类型的对象,SimpleVue上下文可以通过每个函数中的 this 访问。computed属性执行一些计算并返回结果。计算值在上下文中而不是在函数中公开。

  • methods 是一个属性值为函数类型的对象,SimpleVue上下文可以通过每个函数中的 this 访问。在每个 method 函数中,您可以通过 this 上下文访问 data 、 computed 或其他 methods 。 computed 和 methods 的区别在于 methods 在上下文中按原样作为函数公开。

SimpleVue 函数的使用示例如下:

SimpleVue({  data() {    // @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.amount)      alert(this.fullname.toLowerCase())      alert(this.getRandom())    },    test() {      const fullname = this.fullname      const cases: [Expect<Equal<typeof fullname, string>>] = [] as any    },  },})

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

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

type Expect<T extends true> = Ttype Equal<X, Y> =  (<T>() => T extends X ? 1 : 2) extends  (<T>() => T extends Y ? 1 : 2) ? true : false


解决方案

首先,我们使用 declare 关键字声明 SimpleVue 函数。它接受 options 形参,其初始类型为 any 类型。

declare function SimpleVue(options: any): any

从前面的挑战描述可以看出, options 的参数类型是一个对象类型,包含三个属性: data 、 computed 和 methods 。 data 属性的类型是函数类型,调用函数后将返回一个对象。由于返回的对象是未知的,我们需要引入一个类型参数来指示返回对象的类型。这里我们选择的类型参数的名称是 Data Data,当然,您也可以使用其他名称。

另外,我们希望在调用 data 函数后返回一个对象。因此,可以通过 extends 关键字约束 Data 类型形参的类型。

declare function SimpleVue<  Data extends Record<string, any>>(options: {    data: () => Data}): any

在 data 函数内部,您不能访问其他计算属性或方法。为了实现这个约束,我们可以引入 this “伪形参”,并将其类型设置为 void ,从而禁止在方法或函数中使用 this 。

定义了 data 属性的类型之后,让我们定义 computed 属性的类型。根据挑战描述和上面的例子, computed 属性的类型是一个对象类型,对象中每个属性的类型是一个函数类型。由于 computed 属性对应的对象类型也是未知的,因此我们还需要引入另一个类型参数来表示它的类型。我们在这里选择的类型参数的名称是 Computed ,但是您也可以使用其他名称。

declare function SimpleVue<  Data extends Record<string, any>,  Computed>(options: {    data: (this: void) => Data,    computed: Computed}): any

现在我们已经定义了 data 和 computed 属性的类型,让我们先验证 SimpleVue 的函数:

SimpleVue({  data() {    // @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}`    },  },})

对于上面的代码,TypeScript编译器会提示以下错误信息:

Property 'firstname' does not exist on type '{ fullname(): any; }'.Property 'lastname' does not exist on type '{ fullname(): any; }'.

那么如何解决上述问题呢?此时,我们需要使用TypeScript内置的 ThisType<Type> 泛型,它用于标记 this 上下文的类型。

// lib/lib.es5.d.tsinterface ThisType<T> { }declare function SimpleVue<  Data extends Record<string, any>,  Computed>(options: {    data: (this: void) => Data,    computed: Computed & ThisType<Data>}): any

当我们将 computed 属性的类型更新为 Computed & ThisType<Data> 类型时,之前的错误信息就消失了。这是因为 fullname 函数中 this 对象的类型为 Data ,这是调用 data 函数后返回值的类型。对于本文中使用的示例,返回值的类型将包含诸如 firstname 和 lastname 之类的属性。

接下来,让我们处理 methods 属性。与前面的 data 和 computed 属性一样,我们需要引入新的类型参数。这里选择的类型形参的名称是 Methods :

declare function SimpleVue<  Data extends Record<string, any>,  Computed,  Methods,>(options: {    data: (this: void) => Data,    computed: Computed & ThisType<Data>    methods: Methods}): any

在挑战描述中,要求在每个 method 函数中,您可以通过 this 上下文访问 data 、 computed 或其他 methods 。为了满足这个要求,我们还需要使用前面使用的 ThisType<Type> 泛型。能够访问 data 和其他 methods 相对容易处理,我们只需要像这样设置泛型参数 ThisType<Data & Methods> 。

declare function SimpleVue<  Data extends Record<string, any>,  Computed,  Methods,>(options: {    data: (this: void) => Data,    computed: Computed & ThisType<Data>    methods: Methods & ThisType<Data & Methods>}): any

为了能够访问 computed 属性,我们不仅需要能够访问所有计算属性,还需要能够访问每个计算属性对应的函数的返回值类型。这样,我们就可以在通过 this.fullname 访问计算属性后得到相应的智能提示。

此时,我们需要使用TypeScript中的映射类型和内置的 ReturnType 泛型类型,该泛型用于获取函数类型的返回值类型。相应的代码如下所示:

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

在上面的代码中,我们使用 ReturnType<Computed[P]> 来获取computed属性的返回类型。上面的代码看起来很好,但是TypeScript编译器会提示以下错误信息:

Type 'Computed[P]' does not satisfy the constraint '(...args: any) => any'.  Type 'Computed[keyof Computed]' is not assignable to type '(...args: any) => any'.    Type 'Computed[string] | Computed[number] | Computed[symbol]' is not assignable to type '(...args: any) => any'.      Type 'Computed[string]' is not assignable to type '(...args: any) => any'.

出现上述错误消息的原因是 ReturnType 泛型中的type参数使用了泛型约束:

// lib/lib.es5.d.tstype ReturnType<T extends (...args: any) => any> =   T extends (...args: any) => infer R ? R : any;

为了解决这个问题,我们可以在 Computed 类型参数中添加相应的约束。

// lib/lib.es5.d.tsdeclare type PropertyKey = string | number | symbol;Computed extends Record<PropertyKey, (...args: unknown[]) => unknown>

在上面的代码中,我们使用了TypeScript内置的 Record 泛型和 PropertyKey 类型。在为 Computed 类型参数添加约束后, ReturnType<Computed[P]> 表达式将不再报告错误。同样,我们也可以为 Methods 类型参数添加相应的约束。

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

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

当然,不用使用TypeScript内置的 ReturnType 泛型,你可以通过条件类型和 infer 类型推断来获得函数类型的返回值类型。具体代码如下:

{  [P in keyof Computed]:    Computed[P] extends      (...args: any[]) => infer R      ? R : never}

在完成 SimpleVue 函数类型定义的过程中,除了使用类型参数和泛型约束外,本文还使用了
this“伪参数”、ThisType<Type> 和 ReturnType 泛型。这些知识并不难。本文的重点是使每个人都能更好地理解类型参数的作用和完成 SimpleVue 函数声明的思想。

 欢迎关注公众号:文本魔术,了解更多

  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值