
泛型在开发第三方库时非常有用
在本文中,我将介绍如何使用TypeScript泛型来声明一个 defineStore 函数(类似于Pinia库中的 defineStore 函数)来完成以下挑战。在挑战中,我还会介绍一些非常有用的TypeScript知识。掌握了以后,应该会对你的工作有所帮助。
-
TypeScript泛型的高级用法第1部分
-
TypeScript泛型的高级用法第2部分
挑战
创建一个类似于Pinia库中的 defineStore 函数的函数。实际上不需要实现函数,只需声明函数的相应类型即可。该函数只接受一个类型为对象的形参。该节点包含4个属性:
-
Id:字符串类型(必选)
-
state:返回一个对象作为
Store(必需的)的状态的函数。 -
getter:一个包含方法的对象,类似于Vue的计算属性或Vue的getter(可选)。
-
动作:包含可以处理副作用和改变状态的方法的对象(可选)。
Getters
当你像这样定义一个Store:
const store = defineStore({// ...other required fieldsgetters: {getSomething() {return 'xxx'}}})
之后,你可以像这样使用 store 对象:
store.getSomething
并且,getters可以通过 this 访问 state 或其他 getters ,但状态为只读。
Actions
当你像这样定义一个Store:
const store = defineStore({// ...other required fieldsactions: {doSideEffect() {this.xxx = 'xxx'return 'ok'}}})
之后,你可以像这样使用 store 对象:
const returnValue = store.doSideEffect()
动作可以返回任何值,也可以不返回任何值,它们可以接收任意数量的不同类型的参数。参数类型和返回类型不能丢失,这意味着在调用端必须进行类型检查。
可以通过Action中的 this 访问和修改状态。虽然getter也可以通过 this 访问,但它们是只读的。
defineStore 函数的使用示例如下:
const store = defineStore({id: '',state: () => ({num: 0,str: '',}),getters: {stringifiedNum() {// @ts-expect-errorthis.num += 1return this.num.toString()},parsedNum() {return parseInt(this.stringifiedNum)},},actions: {init() {this.reset()this.increment()},increment(step = 1) {this.num += step},reset() {this.num = 0// @ts-expect-errorthis.parsedNum = 0return true},setNum(value: number) {this.num = value},},})// @ts-expect-errorstore.nopeStateProp// @ts-expect-errorstore.nopeGetter// @ts-expect-errorstore.stringifiedNum()store.init()// @ts-expect-errorstore.init(0)store.increment()store.increment(2)// @ts-expect-errorstore.setNum()// @ts-expect-errorstore.setNum(Ɖ')store.setNum(3)const r = store.reset()type _tests = [Expect<Equal<typeof store.num, number>>,Expect<Equal<typeof store.str, string>>,Expect<Equal<typeof store.stringifiedNum, string>>,Expect<Equal<typeof store.parsedNum, number>>,Expect<Equal<typeof r, true>>,]
在上面的例子中,使用了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 声明 defineStore 函数,该函数接受初始类型为 any 类型的 options 形参。
declare function defineStore(options: any): any
从前面的挑战描述可以看出, options 参数是一个包含4个属性的对象: id 、 state 、 getters 和 actions ,每个属性的描述如下:
-
Id:字符串类型(必选)
-
state:返回一个对象作为
Store(必需的)的状态的函数。 -
getter:一个包含方法的对象,类似于Vue的计算属性或Vue的getter(可选)。
-
动作:包含可以处理副作用和改变状态的方法的对象(可选)。
基于上述信息,可以为 options 形参定义更精确的类型:
// lib/lib.es5.d.tsdeclare type PropertyKey = string | number | symbol;declare function defineStore<State extends Record<PropertyKey, any>,Getters,Actions>(options: {id: string,state: () => State,getters?: Getters,actions?: Actions}): any
在上面的代码中,我们创建了3个类型参数。 State 类型形参用于表示 state 函数的返回值类型,因为我们期望该函数返回一个对象类型,所以我们在 State 类型形参中添加了相应的约束。在处理了 state 属性之后,让我们来处理 getters 属性。这个时候,我们需要复习一下这个属性的相关描述:
-
当你像这样定义一个Store:
const store = defineStore({// ...other required fieldsgetters: {getSomething() {return 'xxx'}}})
之后,你可以像这样使用 store 对象:
store.getSomething
2. getter可以通过 this 访问 state 或其他 getters ,但状态为只读。
为了能够在返回的 store 对象上访问 getters 对象上定义的方法,我们需要修改 defineStore 函数的返回值类型:
declare function defineStore<State extends Record<PropertyKey, any>,Getters,Actions>(options: {id: string,state: () => State,getters?: Getters,actions?: Actions}): Getters // Change any type to Getters type
之后,可以通过 store 对象访问在 getters 对象上定义的方法:
const store = defineStore({id: '',state: () => ({num: 0,str: '',}),getters: {stringifiedNum() {// @ts-expect-errorthis.num += 1return this.num.toString()}},})store.stringifiedNum() // ✅
为了满足“getter可以通过 this 访问 state 或其他 getters ,但状态为只读”的要求,我们需要继续修改 defineStore 函数的声明。此时,我们需要使用TypeScript内置的 ThisType<Type> 泛型,它用于标记 this 上下文的类型。
declare function defineStore<State extends Record<PropertyKey, any>,Getters,Actions>(options: {id: string,state: () => State,// Use the ThisType generic to mark the type of the `this` contextgetters?: Getters & ThisType<Readonly<State>>actions?: Actions}): Getters
在上面的代码中,我们使用了TypeScript内置的 Readonly 泛型,它用于使对象类型中的属性为只读。将 ThisType 泛型类型添加到 getters 属性的类型后,可以在 getter 函数内部访问 state 函数返回的对象的属性。
const store = defineStore({id: '',state: () => ({num: 0,str: '',}),getters: {stringifiedNum() {// @ts-expect-errorthis.num += 1return this.num.toString()},parsedNum() {return parseInt(this.stringifiedNum) // ❌},},})
在上面的代码中,如果您删除 stringifiedNum 函数中的// @ts-expect-error注释。然后 this.num += 1 表达式将提示以下错误消息:
Cannot assign to 'num' because it is a read-only property.
现在,在 getter 函数中,我们还不能通过 this 访问其他 getters 。实际上, getters 属性类似于本文介绍的 computed 属性。传递 this.stringifiedNum 来获取 stringifiedNum 函数的返回值,而不是获取与 stringifiedNum 属性对应的函数对象。
为了实现上述功能,我们需要在TypeScript中使用映射类型、条件类型和 infer 类型推断。
declare function defineStore<State extends Record<PropertyKey, any>,Getters,Actions>(options: {id: string,state: () => State,// Use the ThisType generic to mark the type of the this contextgetters?: Getters & ThisType<Readonly<State>// Use the return value of the function type corresponding to the key// of the Getter type and the value to form a new object type& {readonly [P in keyof Getters]:Getters[P] extends (...args: unknown[]) => infer R? R : never}>,actions?: Actions}): Getters
需要注意的是,在映射过程中,我们可以通过添加 readonly 修饰符将对象类型的属性设置为只读。为了便于阅读和代码重用,我们可以将上面代码中的映射类型提取为泛型类型:
type ObjectValueReturnType<T> = {readonly [P in keyof T]:T[P] extends (...args: any[]) => infer R? R: never}
对于 ObjectValueReturnType 泛型,我们需要同步更新 defineStore 函数声明:
declare function defineStore<State extends Record<PropertyKey, any>,Getters,Actions>(options: {id: string,state: () => State,// Use the ThisType generic to mark the type of the this contextgetters?: Getters & ThisType<Readonly<State>& ObjectValueReturnType<Getters>>,actions?: Actions}): Getters
处理完 getters 属性后,我们来处理 actions 属性。再一次,让我们回顾一下这个属性的相关描述:
-
当你像这样定义一个Store:
const store = defineStore({// ...other required fieldsactions: {doSideEffect() {this.xxx = 'xxx'return 'ok'}}})
之后,你可以像这样使用 store 对象:
const returnValue = store.doSideEffect()
2. 动作可以返回任何值,也可以不返回任何值,它们可以接收任意数量的不同类型的参数。参数类型和返回类型不能丢失,这意味着在调用端必须进行类型检查。
3. 可以通过Action函数中的 this 对状态进行访问和修改。虽然getter也可以通过 this 访问,但它们是只读的。
为了能够在返回的 store 对象上访问 actions 对象上定义的方法,我们需要继续修改 defineStore 函数的返回值类型:
declare function defineStore<State extends Record<PropertyKey, any>,Getters,Actions>(options: {id: string,// omit other propertiesactions?: Actions}): Getters & Actions // Add Actions type
之后,可以通过 store 对象访问在 actions 对象上定义的方法:
const store = defineStore({id: '',state: () => ({num: 0,str: '',}),actions: {init() {this.reset()this.increment()},// omit other methods},})store.init(); // ✅
为了满足Action中“状态可以通过 this 访问和更改”的要求。并且getter也可以通过 this 访问,但它们是只读的。”的要求,我们需要使用前面使用的 ThisType 泛型类型:
declare function defineStore<State extends Record<PropertyKey, any>,Getters,Actions>(options: {id: string,state: () => State,getters?: Getters & ThisType<Readonly<State>& ObjectValueReturnType<Getters>>,actions?: Actions & ThisType<State // Can access and change state& ObjectValueReturnType<Getters>>}): Getters & Actions // Can access properties in Getters
此外,该用法示例还允许我们通过 action 函数内部的 this 上下文访问其他 action 函数。因此,我们需要更新 actions 属性的类型:
actions?: Actions & ThisType<State& Actions // Allow access to other actions through this object& ObjectValueReturnType<Getters>>
当前的 defineStore 函数声明已经满足了使用示例的大部分要求。然而,需要进一步的调整来满足以下所有测试用例:
type _tests = [Expect<Equal<typeof store.num, number>>, // ❌Expect<Equal<typeof store.str, string>>, // ❌Expect<Equal<typeof store.stringifiedNum, string>>, // ❌Expect<Equal<typeof store.parsedNum, number>>, // ❌Expect<Equal<typeof r, true>>, // ✅]
从上面的测试用例可以看出, defineStore 函数创建的 store 对象也可以访问 state 函数的返回值。此外, store 对象还可以访问在 getters 对象中定义的属性。通过 store.stringifiedNum 或 store.parsedNum 访问相应属性的值。之后,可以通过 typeof 操作符获得属性的类型。
为了实现上述功能,我们需要修改 defineStore 函数的返回值类型:
declare function defineStore<State extends Record<PropertyKey, any>,Getters,Actions>(options: {id: string,state: () => State,// omit other properties}): State // The return type is set to the State type& ObjectValueReturnType<Getters>& Actions
最后,让我们看一下完整的代码:
type ObjectValueReturnType<T> = {readonly [P in keyof T]:T[P] extends (...args: any[]) => infer R? R: never}declare function defineStore<State extends Record<PropertyKey, any>,Getters,Actions>(options: {id: string,state: () => State,getters?: Getters & ThisType<Readonly<State>& ObjectValueReturnType<Getters>>,actions?: Actions & ThisType<State& Actions& ObjectValueReturnType<Getters>>}): State& ObjectValueReturnType<Getters>& Actions
类型参数是根据需要引入的,它们只是类型占位符。由您来决定什么类型或在哪里放置类型参数。例如, defineStore 函数中的 State 类型参数用于表示 state 函数的返回值类型。 Getters 和 Actions 类型参数分别用于表示 getters 和 actions 属性的类型。如果您想了解更多关于类型参数的信息,可以阅读下面的文章。
TypeScript泛型里的T, K 和 V 是什么意思当你第一次看到 TypeScript 泛型中的 T 时,是否觉得奇怪?图中的 T 被称为泛型类型参数,它是我们希望传递给恒等函数的类型占位符。就像传递参数一样,我们取用户指定的实际类型,并将其链接到参数类型和返回值类型。
https://mp.weixin.qq.com/s?__biz=MzU3NjM0NjY0OQ==&mid=2247484438&idx=1&sn=44cc9b3f1520584f985c7b34df4795c8&chksm=fd140b60ca6382769c739469bca1db5c0770b9eb7038ea0b62c3bd60da2b64abcf270f8620a7&token=1779636375&lang=zh_CN#rd
欢迎关注公众号:文本魔术,了解更多
