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

news2025/6/13 20:56:55

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

在本文中,我将介绍如何使用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.coloradmin.cn/o/1383997.html

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

相关文章

BRC20通证的深度科普:它的潜力与如何导入到bitget

​BRC-20通证是什么&#xff1f; BRC-20通证&#xff1a;比特币上的“变形金刚”&#xff1f;&#xff01;不依赖智能合约&#xff0c;它们就像拥有超能力的外星人&#xff0c;直接在比特币的最小单位——聪上刻写JSON代码。哈哈&#xff0c;这比把房子建在乐高积木上还要刺激…

50天精通Golang(第17天)

beego框架总结及数据库连接配置 一、beego框架总结 1.1 Beego项目组织架构 上节课程内容对beego的案例代码进行了一个简单的分析&#xff0c;总结一下beego项目的组织结构&#xff0c;总结如下&#xff1a; 1.1.1 项目配置&#xff1a;conf 项目配置文件所在的目录&#x…

华为端口安全常用3种方法配置案例

安全动态mac地址学习功能 [Huawei]int g0/0/01 interface GigabitEthernet0/0/1 port-security enable //开启安全 port-security max-mac-num 2 //最多为2个mac地址学习 port-security protect-action restrict //丢包带警告 port-security aging-time 1 //mac地址的老化时间…

漏洞复现--GitLab 任意用户密码重置漏洞(CVE-2023-7028)

免责声明&#xff1a; 文章中涉及的漏洞均已修复&#xff0c;敏感信息均已做打码处理&#xff0c;文章仅做经验分享用途&#xff0c;切勿当真&#xff0c;未授权的攻击属于非法行为&#xff01;文章中敏感信息均已做多层打马处理。传播、利用本文章所提供的信息而造成的任何直…

懒得玩游戏--帮我做数独

目录 简介自动解数独思路核心思路输入解析打印 完整代码 简介 最近玩上了一款类似于数独的微信小程序游戏&#xff0c;名字叫数独趣味闯关&#xff0c;过了数独的关卡之后会给拼图&#xff0c;玩了几关之后摸清套路了就有点累了&#xff0c;但是还想集齐拼图&#xff0c;所以就…

Windows之任意文件删除到提权

前言 ZDI 发表过从任意文件夹删除到提权的利用过程&#xff0c;还提供了任意文件删除到提权的利用过程&#xff0c;所以一字之差但是漏洞利用方式也是有细微偏差的。 这里把任意文件删除和任意文件夹删除漏洞提权结合起来分析&#xff0c;是因为其最后的利用过程是一样的&…

MongoDB - 索引底层原理和使用,聚合的使用(案例 + 演示)

目录 一、MongoDB 索引 1.1、说明 1.2、原理 1.3、操作 1.3.1、创建索引 1.3.2、查看集合索引列表 1.3.3、查看集合索引大小 1.3.4、删除集合所有索引 1.3.5、删除集合指定索引 1.3.6、创建复合索引 1.4、聚合 a&#xff09; 统计每个作者写的文章数 b&#xff09…

【NI国产替代】PXIe‑6375,208路AI(16位,3.8 MS/s),2路AO,24路DIO,PXI多功能I/O模块

PXIe&#xff0c;208路AI&#xff08;16位&#xff0c;3.8 MS/s&#xff09;&#xff0c;2路AO&#xff0c;24路DIO&#xff0c;PXI多功能I/O模块 PXIe‑6375提供了模拟I/O、数字I/O和四个32位计数器/定时器&#xff0c;用于PWM、编码器、频率、事件计数等应用。 该设备利用高吞…

通过本质看现象:关于Integer受内部初始化赋值范围限制而出现的有趣现象

文/朱季谦 这是我很多年前的第一篇技术博客&#xff0c;当时作为一名技术小菜鸟&#xff0c;总体而言显得很拙见&#xff0c;但也算是成长路上的一个小脚印&#xff0c;希望能在以后的日子里&#xff0c;可以对JAVA技术有一个更加深入的思考与认识。 前几天我在逛论坛的时候&a…

webpack执行流程知识点总结

webpack的运行流程 Webpack 的运行流程是一个串行的过程&#xff0c;从启动到结束会依次执行以下流程&#xff1a; 在以上过程中&#xff0c;Webpack 会在特定的时间点广播出特定的事件&#xff0c;插件在监听到感兴趣的事件后会执行特定的逻辑&#xff0c;并且插件可以调用 We…

网工内推 | 信息安全主管,CISP/CISSP认证优先,最高25K

01 武汉华康世纪医疗股份有限公司 招聘岗位&#xff1a;网络安全主管 职责描述&#xff1a; 1、推进公司信息/网络安全管理体系规划、建设、持续改进&#xff0c;促进信息安全管理的推行落地,保障网络、系统与数据安全&#xff1b; 2、维护管理信息/网络管理软件&#xff0c;设…

Centos7,Python3.7.6安装模块Crypto,pycryptodome,ibm_db,requests,requests_pkcs12

Centos7,Python3.7.6安装模块Crypto&#xff0c;pycryptodome&#xff0c;ibm_db&#xff0c;requests,requests_pkcs12 Python版本&#xff1a;python3.7.6 对应的各种模块 前言&#xff1a;把python项目放到linux上运行时&#xff0c;提示缺少各种模块&#xff0c;安装命令…

openFeign 多模块调用失败问题

第一次做一个完整的SpringCloud微服务项目,踩了好多好多坑,都记录下来! openFeign 多模块调用失败 排错第一阶段 创建一个openfeign服务,并把它注册到nacos上去 然后A模块通过Feign调用B模块 但是我在A模块实现AdminArticleServiceFeignClient这个接口,报错: 后面我查找这个问…

OSPF : 区域 / 为什么非骨干互访需要经过骨干

概述 OSPF系列第二篇 , 今天来围绕着区域这个概念展开写一篇博客 分区背景 先来讨论一下技术背景 , 也就是为什么要分区 ? 所有设备都在一个区域不行吗 会有什么问题呢 . 首先明确一个知识点 : 正常状态下一个区域内的所有设备的LSDB都是一样的.区域内的路由器必须为所属的…

小程序基础学习(请求封装)(重点,核心)

目录 首先&#xff1a; 封装一个request请求的js文件&#xff0c;用的是Promise 然后&#xff1a; 请求编写 原理&#xff1a;首先在页面加载完成以后发送一次请求数据&#xff0c;由于请求的数据会反复使用&#xff0c;直接把他抽离到外面&#xff0c;以后直接调用。在使用a…

如何用MetaGPT帮你写一个贪吃蛇的小游戏项目

如何用MetaGPT帮你写一个贪吃蛇的小游戏项目 MetaGPT是基于大型语言模型(LLMs)的多智能体写作框架&#xff0c;目前在Github开源&#xff0c;其Start数量也是比较高的&#xff0c;是一款非常不错的开源框架。 下面将带你进入MetaGPT的大门&#xff0c;开启MetaGPT的体验之旅。…

使用android studio编译app到自己的手机上运行,却读取不了手机里面的图片

问题描述&#xff1a; 使用android studio编译app到自己的手机上运行&#xff0c;却读取不了手机里面的图片 问题分析&#xff1a; 这个是由于这个app没有申请手机端的 媒体文件访问权限&#xff0c;所以读取不了 解决&#xff1a;&#xff08;我的是Android 10&#xff0c;新版…

数据结构学习之对单向链表进行快速高效的排序(排序链表)

实例要求&#xff1a;1、给定一个链表的头结点 head &#xff0c;请将其按升序排列并返回排序后的链表 &#xff1b;2、链表中节点的数目的范围为 [0, 5 * 104] &#xff1b;实例分析&#xff1a;1、引入qsort函数和自定义cmp函数解决问题&#xff1b;2、借助自定义指针作为中间…

Android Studio下载gradle反复失败

我的版本&#xff1a;gradle-5.1.1 首先检查设置路径是否正确&#xff0c;参考我的修改&#xff01; 解决方案 1.手动下载Gradle.bin Gradle Distributions 下载地址 注意根据编译器提示下载&#xff0c;我这要求下载的是bin 而不是all 2.把下载好的整个压缩包放在C:\Users\…

jenkins-cl参数化构建

pipeline片段&#xff08;对应jenkins-cli -p参数的BRANCHdevelop&#xff09; parameters {string(name: BRANCH, defaultValue: master, description: Enter the branch name)}stages {stage(Get Code) {steps {script {def branch params.BRANCHcheckout scmGit(branches: …