泛型在开发组件或库时非常有用
在本文中,我将介绍如何使用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> = T
type 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.ts
interface 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.ts
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
为了解决这个问题,我们可以在 Computed
类型参数中添加相应的约束。
// lib/lib.es5.d.ts
declare 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
函数声明的思想。
欢迎关注公众号:文本魔术,了解更多