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

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

在本文中,我将介绍如何使用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 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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/620848.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

用 AI 生成 Vue 组件?

大家好我是奇兵&#xff0c;三个月前&#xff0c;Vercel 推出了其 AI 代码生成工具 —— v0&#xff08;v0.dev&#xff09;&#xff0c;这款工具可以快速生成前端组件代码&#xff0c;引起了前端圈的广泛关注。通过简单的描述&#xff0c;v0能够快速生成前端页面&#xff0c;让…

算法第十八天-实现Trie(前缀树)

实现Trie&#xff08;前缀树&#xff09; 题目要求 解题思路 本文是前缀入门教程 从二叉树说起 前缀树&#xff0c;也是一种树。为了理解前缀树&#xff0c;我们先从二叉树说起。常见的二叉树结构是下面这样子的&#xff1a; class TreeNode { int val; TreeNode* left; Tre…

CDSP和CISP证书,选择哪个?

&#x1f3af;CDSP和CISP是两种与信息安全领域相关的专业认证。它们有一些相似之处&#xff0c;但也存在一些显著的区别。本文将详细介绍CDSP认证和CISP认证的相同点和区别。 &#x1f451;CDSP和CISP的相同点&#xff1a; 1.行业认可&#xff1a;CDSP和CISP都是行业广泛认可的…

Linux下通过EDAC功能检测PCIE硬件错误

1 EDAC的作用 The edac kernel modules goal is to detect and report hardware errors that occur within the computer system running under linux. 《Documentation/admin-guide/ras.rst》 EDAC可以检测物理内存的错误 和 PCIE的错误&#xff0c;本文主要分析后者。 2 机…

如何使用WinDiff浏览和对比Windows源代码中的符号和系统调用信息

关于WinDiff WinDiff是一款功能强大的Windows二进制源代码安全分析与调试工具&#xff0c;该工具完全开源&#xff0c;基于Web实现其功能&#xff0c;可以帮助广大研究人员在不同版本的操作系统中浏览和对比Microsoft Windows二进制文件的符号、类型和系统调用信息。其中&…

详解HTTPS加密工作过程

&#x1f697;&#x1f697;&#x1f697;今天给大家分享的是HTTPS加密的工作过程。 清风的CSDN博客 &#x1f6e9;️&#x1f6e9;️&#x1f6e9;️希望我的文章能对你有所帮助&#xff0c;有不足的地方还请各位看官多多指教&#xff0c;大家一起学习交流&#xff01; ✈️✈…

PDCA/绩效管理活动

现代绩效管理理论认为&#xff0c;绩效管理活动是一个连续的过程&#xff0c;是指管理者用来确保自己下属员工的工作行为和工作产出与组织的目标保持一致的手段及过程。人们通常用一个循环过程来描述绩效管理的整个过程。我们认为&#xff0c;一个组织的员工绩效管理活动由四个…

NUS CS1101S:SICP JavaScript 描述:一、使用函数构建抽象

原文&#xff1a;1 Building Abstractions with Functions 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 心灵的行为&#xff0c;其中它对简单的想法施加其力量&#xff0c;主要有以下三种&#xff1a;1.将几个简单的想法组合成一个复合的想法&#xff0c;从而形成所…

【深度学习】RTX2060 2080如何安装CUDA,如何使用onnx runtime

文章目录 如何在Python环境下配置RTX 2060与CUDA 101. 安装最新的NVIDIA显卡驱动2. 使用conda安装CUDA Toolkit3. 验证onnxruntime与CUDA版本4. 验证ONNX需求版本5. 安装ONNX与onnxruntime6. 编写ONNX推理代码 如何在Python环境下配置RTX 2060与CUDA 10 RTX 2060虽然是一款较早…

Ps:何时需要转换为智能对象

智能对象 Smart Objects提供了广泛的灵活性和控制能力&#xff0c;特别是在处理复杂的合成、重复元素或需要非破坏性编辑的项目中。 ◆ ◆ ◆ 何时需要转换为智能对象 1、当需要对图像进行缩放、旋转等变换时。 涉及到的 Photoshop 命令包括&#xff1a;变换、自由变换、操控…

windows下如何搭建Yapi环境

今天使用YApi时发现原网址无法访问。这下只能本地部署了&#xff08;官方文档&#xff09;。 第一步&#xff1a;安装node.js 获取资源 nodejs: https://nodejs.org/en/downloadLinux安装yum install -y nodejs查看node版本node -v查看npm版本npm -v第二步&#xff1a;安装mo…

【论文阅读笔记】MobileSal: Extremely Efficient RGB-D Salient Object Detection

1.介绍 MobileSal: Extremely Efficient RGB-D Salient Object Detection MobileSal&#xff1a;极其高效的RGB-D显著对象检测 2021年发表在 IEEE Transactions on Pattern Analysis and Machine Intelligence。 Paper Code 2.摘要 神经网络的高计算成本阻碍了RGB-D显着对象…

Vue 自定义仿word表单录入之日期输入组件

因项目需要&#xff0c;要实现仿word方式录入数据&#xff0c;要实现鼠标经过时才显示编辑组件&#xff0c;预览及离开后则显示具体的文字。 鼠标经过时显示 正常显示及离开时显示 组件代码 <template ><div class"paper-input flex flex-col border-box "…

Matlab字符识别实验

Matlab 字符识别OCR实验 图像来源于屏幕截图&#xff0c;要求黑底白字。数据来源是任意二进制文件&#xff0c;内容以16进制打印输出&#xff0c;0-9a-f’字符被16个可打印字符替代&#xff0c;这些替代字符经过挑选&#xff0c;使其相对容易被识别。 第一步进行线分割和字符…

AI编程可视化Java项目拆解第一弹,解析本地Java项目

之前分享过一篇使用 AI 可视化 Java 项目的文章&#xff0c;同步在 AI 破局星球、知乎、掘金等地方都分享了。 原文在这里AI 编程&#xff1a;可视化 Java 项目 有很多人感兴趣&#xff0c;我打算写一个系列文章拆解这个项目&#xff0c;大家多多点赞支持~ 今天分享的是第一…

alibaba学习笔记03(小滴课堂)

自定义Ribbon负载均衡策略实战 启动3个视频服务和一个订单服务&#xff1a; 我们可以看到它是随机调用的。 也可以使用其他负载均衡策略。 讲解新一代负载均衡组件feign介绍 这种方式去写死接口肯定是不妥当的。 于是我们使用feign负载均衡组件&#xff1a; 改造微服务 集成F…

【Linux】 系统目录结构

进入到根目录 cd /ls目录名具体作用/存放系统系统相关的目录文件/boot放置linux系统内核文件和启动时用到的一些引导文件/home包含linux系统上各用户的主目录&#xff0c;子目录名称默认以该用户名命名/root系统管理员root的家目录/bin包含常用的命令文件&#xff08;如ls 等&a…

CBA业务架构师认证考试含金量

CBA业务架构师认证考试的含金量主要体现在以下几个方面&#x1f447; 1️⃣权威性 &#x1f48e;CBA业务架构师是业务架构师协会提供了一项国际认证计划&#xff0c;该计划可以衡量业务架构师的能力&#xff0c; 并向证明公认的熟练程度的个人授予认证业务架构师(Certified Bus…

vue前端开发自学,祖孙多层级组件嵌套关系数据传输

vue前端开发自学,祖孙多层级组件嵌套关系数据传输&#xff01;官方提供了一个解决方案&#xff0c;就是&#xff0c;在根组件内使用provide,哪个子孙组件想调用这个数据&#xff0c;就可以inject接收就行了。虽然是方便了&#xff0c;但是这个有点要求&#xff0c;就是只能自上…

初识 Elasticsearch 应用知识,一文读懂 Elasticsearch 知识文集(3)

&#x1f3c6;作者简介&#xff0c;普修罗双战士&#xff0c;一直追求不断学习和成长&#xff0c;在技术的道路上持续探索和实践。 &#x1f3c6;多年互联网行业从业经验&#xff0c;历任核心研发工程师&#xff0c;项目技术负责人。 &#x1f389;欢迎 &#x1f44d;点赞✍评论…