泛型在开发第三方库时非常有用
在本文中,我将介绍如何使用TypeScript泛型来声明一个?defineStore
?函数(类似于Pinia库中的?defineStore
?函数)来完成以下挑战。在挑战中,我还会介绍一些非常有用的TypeScript知识。掌握了以后,应该会对你的工作有所帮助。
创建一个类似于Pinia库中的?defineStore
?函数的函数。实际上不需要实现函数,只需声明函数的相应类型即可。该函数只接受一个类型为对象的形参。该节点包含4个属性:
Id:字符串类型(必选)
state:返回一个对象作为?Store
?(必需的)的状态的函数。
getter:一个包含方法的对象,类似于Vue的计算属性或Vue的getter(可选)。
动作:包含可以处理副作用和改变状态的方法的对象(可选)。
当你像这样定义一个Store:
const store = defineStore({
// ...other required fields
getters: {
getSomething() {
return 'xxx'
}
}
})
之后,你可以像这样使用?store
?对象:
store.getSomething
并且,getters可以通过?this
?访问?state
?或其他?getters
?,但状态为只读。
Actions
当你像这样定义一个Store:
const store = defineStore({
// ...other required fields
actions: {
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-error
this.num += 1
?
return 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-error
this.parsedNum = 0
?
return true
},
setNum(value: number) {
this.num = value
},
},
})
?
// @ts-expect-error
store.nopeStateProp
// @ts-expect-error
store.nopeGetter
// @ts-expect-error
store.stringifiedNum()
store.init()
// @ts-expect-error
store.init(0)
store.increment()
store.increment(2)
// @ts-expect-error
store.setNum()
// @ts-expect-error
store.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> = T
type 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.ts
declare 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 fields
getters: {
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-error
this.num += 1
return 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` context
getters?: 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-error
this.num += 1
return 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 context
getters?: 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 context
getters?: Getters & ThisType<
Readonly<State>
& ObjectValueReturnType<Getters>>,
actions?: Actions
}): Getters
处理完?getters
?属性后,我们来处理?actions
?属性。再一次,让我们回顾一下这个属性的相关描述:
当你像这样定义一个Store:
const store = defineStore({
// ...other required fields
actions: {
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 properties
actions?: 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
?属性的类型。如果您想了解更多关于类型参数的信息,可以阅读下面的文章。
?欢迎关注公众号:文本魔术,了解更多