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

发布时间:2024年01月14日

泛型在开发第三方库时非常有用

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


挑战
?

创建一个类似于Pinia库中的?defineStore?函数的函数。实际上不需要实现函数,只需声明函数的相应类型即可。该函数只接受一个类型为对象的形参。该节点包含4个属性:

  • Id:字符串类型(必选)

  • state:返回一个对象作为?Store?(必需的)的状态的函数。

  • getter:一个包含方法的对象,类似于Vue的计算属性或Vue的getter(可选)。

  • 动作:包含可以处理副作用和改变状态的方法的对象(可选)。

Getters


当你像这样定义一个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-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?属性。这个时候,我们需要复习一下这个属性的相关描述:

  1. 当你像这样定义一个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?属性。再一次,让我们回顾一下这个属性的相关描述:

  1. 当你像这样定义一个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?属性的类型。如果您想了解更多关于类型参数的信息,可以阅读下面的文章。

TypeScript泛型里的T, K 和 V 是什么意思当你第一次看到 TypeScript 泛型中的 T 时,是否觉得奇怪?图中的 T 被称为泛型类型参数,它是我们希望传递给恒等函数的类型占位符。就像传递参数一样,我们取用户指定的实际类型,并将其链接到参数类型和返回值类型。icon-default.png?t=N7T8https://mp.weixin.qq.com/s?__biz=MzU3NjM0NjY0OQ==&mid=2247484438&idx=1&sn=44cc9b3f1520584f985c7b34df4795c8&chksm=fd140b60ca6382769c739469bca1db5c0770b9eb7038ea0b62c3bd60da2b64abcf270f8620a7&token=1779636375&lang=zh_CN#rd

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

文章来源:https://blog.csdn.net/wannianchuan/article/details/135560477
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。