Typescript 中,这些类型工具真好用

news2024/10/5 13:09:11

你是否曾经用 TypeScript 写代码,然后意识到这个包没有导出我需要的类型,例如下面这段代码提示 Content@example 中不存在:

在这里插入图片描述

import {getContent, Content} from '@example'
const content = await getContent()
function processContent(content:Content) {
 // ...
}

幸运的是,TypeScript 为我们提供了许多可以解决这个常见问题的类型工具,详细的可以参考官方文档给出的 utility 类型。

例如,要获取函数返回的类型,我们可以使用 ReturnType

import { getContent } from '@example'
const content = await getContent()

type Content = ReturnType<typeof getContent>

但有一个小问题getContent 是一个返回 promiseasync 函数,所以目前我们的Content 类型实际上是 promise,这不是我们想要的。

为此,我们可以使用 await 类型来解析 promise,并获得 promise resolve 的类型:

import { getContent } from '@example'
const content = await getContent()

type Content = Awaited<ReturnType<typeof getContent>>

现在我们有了所需的确切类型。

但是如果我们需要这个函数的参数类型呢?

例如,getContent 接受一个名为 ContentKind 的可选参数,该参数是字符串的并集。但我真的不想手动输入这些,那可以让我们使用 Parameters 类型工具来提取它的参数:

type Arguments = Parameters<typeof getContent>
// [ContentKind | undefined]

Parameters 会返回给你一个参数类型的元组,你可以通过索引提取一个特定的参数类型,如下所示:

type ContentKind = Parameters<typeof getContent>[0]

但我们还有最后一个问题。因为这是一个可选参数,我们的 ContentKind 类型现在实际上是 ContentKind | undefined,这不是我们想要的。为此,我们可以使用NonNullable 类型工具,从联合类型中排除空值或未定义值:

type ContentKind = NonNullable<Parameters<typeof getContent>[0]>
// ContentKind

现在我们的 ContentKind 类型与这个包中没有导出的 ContentKind 完全匹配,我们可以在 processContent 函数中使用它了:

import { getContent } from '@example'

const content = await getContent()

type Content = Awaited<ReturnType<typeof getContent>>
type ContentKind = NonNullable<Parameters<typeof getContent>[0]>

function processContent(content: Content, kind: ContentKind) {
  // ...
}

在 React 中使用工具类型

工具类型也可以在 React 组件方面给我们很大的帮助。

例如,下面我有一个编辑日历事件的简单组件,我们在其中维护一个处于状态的事件对象,并在发生变化时修改事件标题。

你能发现下面这段代码中的错误吗?

import React, { useState } from 'react'

type Event = { title: string, date: Date, attendees: string[] }

export function EditEvent() {
  const [event, setEvent] = useState<Event>()
  return (
    <input 
      placeholder="Event title"
      value={event.title} 
      onChange={e => {
        event.title = e.target.value
      }}
    />
  )
}

上面的代码,我们正在直接改变事件对象。这将导致我们的输入不能像预期的那样工作,因为 React 不会意识到状态的变化,因此不会呈现变化。

我们需要做的是用一个新对象调用 setEvent

那你可能突然会问:为什么 TypeScript 没有捕捉到这个错误呢?

从技术上讲,你可以用 useState 改变对象。不过,我们可以先通过使用 Readonly 类型工具来提高类型安全性,以强制我们不应该改变该对象的任何属性:

const [event, setEvent] = useState<Readonly<Event>>()

现在我们之前的错误将自动被捕获:

export function EditEvent() {
  const [event, setEvent] = useState<Readonly<Event>>()
  return (
    <input 
      placeholder="Event title"
      value={event.title} 
      onChange={e => {
        event.title = e.target.value
        //   ^^^^^ Error: Cannot assign to 'title' because it is a read-only property
      }}
    />
  )
}

接着,看到报错的你,有改了代码:

<input
  placeholder="Event title"
  value={event.title} 
  onChange={e => {
    // ✅
    setState({ ...event, title: e.target.value })
  }}
/>

但是,这仍然存在一个问题。Readonly 仅适用于对象的顶层属性。我们仍然可以改变嵌套的属性和数组而不会出现错误:

export function EditEvent() {
  const [event, setEvent] = useState<Readonly<Event>>()
  // ...

  // TypeScript 不会报错,尽管这是一个错误
  event.attendees.push('foo')
}

但是,现在我们知道了 Readonly,我们可以把它和它的兄弟类型 ArrayReadonly 结合起来,再加上一点魔法,创建我们自己的 DeepReadonly 类型,像这样:

export type DeepReadonly<T> =
  T extends Primitive ? T :
  T extends Array<infer U> ? DeepReadonlyArray<U> :
  DeepReadonlyObject<T>

type Primitive = 
  string | number | boolean | undefined | null

interface DeepReadonlyArray<T> 
  extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>
}

现在,使用 DeepReadonly,我们就不能改变整个树中的任何东西了:

export function EditEvent() {
  const [event, setEvent] = useState<DeepReadonly<Event>>()
  // ...

  event.attendees.push('foo')
  //             ^^^^ Error!
}

只有在这样的情况下才会通过类型检查:

export function EditEvent() {
  const [event, setEvent] = useState<DeepReadonly<Event>>()
  
  // ...
  
  // ✅
  setEvent({
    ...event,
    title: e.target.value,
    attendees: [...event.attendees, 'foo']
  })
}

对于这种复杂性,你可能想要使用的另一种模式,即将此逻辑移动到自定义钩子中的做法:

function useEvent() {
  const [event, setEvent] = useState<DeepReadonly<Event>>()
  function updateEvent(newEvent: Event) {
    setEvent({ ...event, newEvent })
  }
  return [event, updateEvent] as const
}

export function EditEvent() {
  const [event, updateEvent] = useEvent()
  return (
    <input 
      placeholder="Event title"
      value={event.title} 
      onChange={e => {
        updateEvent({ title: e.target.value })
      }}
    />
  )
}

但是会有一个新的问题。updateEvent 期望得到完整的事件对象,但是我们想要的只是一个部分对象,所以我们会得到下面这样的错误:

updateEvent({ title: e.target.value })
//        ^^^^^^^^^^^^^^^^^^^^^^^^^ Error: Type '{ title: string; }' is missing the following properties from type 'Event': date, attendees

幸运的是,这很容易用 Partial 类型工具解决,它使所有属性都是可选的:

// ✅
function updateEvent(newEvent: Partial<Event>) { /* ... */ }
// ...
updateEvent({ title: e.target.value })

除了 Partial 之外,还需要了解 Required 类型工具,它的作用正好相反:接受对象上的任何可选属性,并使它们都是必需的。

或者,如果我们只希望某些键被允许包含在我们的 updateEvent 函数中,我们可以使用 Pick 类型工具来指定允许的键:

function updateEvent(newEvent: Pick<Event, 'title' | 'date'>) { /* ... */ }
updateEvent({ attendees: [] })
//          ^^^^^^^^^^^^^^^^^ Error: Object literal may only specify known properties, and 'attendees' does not exist in type 'Partial<Pick<Event, "title" | "date">>'

或者类似地,我们可以使用 Omit 来省略指定的键:

function updateEvent(newEvent: Omit<Event, 'title' | 'date'>) { /* ... */ }
updateEvent({ title: 'Builder.io conf' })
// ✅        ^^^^^^^^^^^^^^^^^ Error: Object literal may only specify known properties, and 'title' does not exist in type 'Partial<Omit<Event, "title">>'

更多类型工具

Record<KeyType, ValueType>

创建一个类型来表示具有给定类型值的任意键的对象:

const months = Record<string, number> = {
  january: 1,
  february: 2,
  march: 3,
  // ...
}

Exclude<UnionType, ExcludedMembers>

从联合类型中删除可分配给 ExcludeMembers 类型的所有成员:

type Months = 'january' | 'february' | 'march' | // ...
type MonthsWith31Days = Exclude<Months, 'april' | 'june' | 'september' | 'november'>
// 'january' | 'february' | 'march' | 'may' ...

Extract<Union, Type>

从联合类型中删除不能分配给 Type 的所有成员:

type Extracted = Extract<string | number, (() => void), Function>
// () => void

ConstructorParameters

Parameters 一样,但用于构造函数:

class Event {
  constructor(title: string, date: Date) { /* ... */ }
}
type EventArgs = ConstructorParameters<Event>
// [string, Date]

InstanceType

给出构造函数的 instance 类型:

class Event { ... }
type Event = InstanceType<typeof Event>
// Event

ThisParameterType

为函数提供 this 参数的类型,如果没有提供则为 unknown

function getTitle(this: Event) { /* ... */ }
type This = ThisType<typeof getTitle>
// Event

OmitThisParameter

从函数类型中删除 this 参数:

function getTitle(this: Event) { /* ... */ }
const getTitleOfMyEvent: OmitThisParameter<typeof getTitle> = 
  getTitle.bind(myEvent)

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

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

相关文章

8.2 电压比较器(1)

电压比较器是对输入信号进行鉴幅与比较的电路&#xff0c;是组成非正弦波发生电路的基本单元电路&#xff0c;在测量和控制中有着相当广泛的应用。 一、概述 1、电压比较器的电压传输特性 电压比较器的输出电压 u O u_{\scriptscriptstyle O} uO​ 与输入电压 u I u_{\scr…

软件测试面试一周时间面了6家公司,最终收割3个Offer

上个月花了一周时间面了6家公司&#xff0c;最终收割3个Offer。通过这6次面试&#xff0c;得到的最宝贵的经验是&#xff1a;快刀斩乱麻。展开说就是&#xff1a;给自己一点点压力&#xff0c;在短时间内迅速面试、迅速反馈、迅速提高&#xff0c;然后迅速进入下一场面试。 以…

一文理清最小二乘法估计

1 最小二乘法估计(LS) 1.1 原理与推导 最小二乘法最早是高斯在预估星体轨道时提出来的&#xff0c;后来成为了估计理论的奠基石。考虑如下CAR模型&#xff1a; 其中&#xff1a; 参数估计的任务就是根据输入和输出&#xff0c;估计出a1,a2,----,ana,b1,b2,...,bnb这nanb1个参…

【MySQL数据库】MySQL日志管理、备份与恢复

MySQL日志管理、备份与恢复 一、MySQL日志管理1.1日志存放位置 二、数据备份2.1物理备份与逻辑备份2.2完整备份、差异备份、增量备份2.3常见的备份方法 三、完整备份与恢复3.1物理冷备份与恢复3.2mysqldump 备份3.3mysqldump数据恢复3.4MySQL增量备份3.5MySQL增量恢复 一、MySQ…

51单片机 - 期末复习重要图

AT89S51片内硬件结构 1.内部硬件结构图 2.内部部件简单介绍 3. 26个特殊功能寄存器分类 按照定时器、串口、通用I/O口和CPU 中断相关寄存器&#xff1a;3IE - 中断使能寄存器IP - 中断优先级寄存器 定时器相关寄存器6TCON - 定时器/计数器控制寄存器TMOD - 定时器/计数器模…

数字图像处理(三)

目录 实验六、图像分割方法 实验七、图像识别与分类 实验六、图像分割方法 一、实验目的 了解图像分割技术相关基础知识&#xff1b;掌握几种经典边缘检测算子的基本原理、实现步骤理解阈值分割、区域分割等的基本原理、实现步骤。理解分水岭分割方法的基本原理、实现方法。…

清华大学实验室走在科研管理前沿,与Zoho合作推进新模式

在教育科研工作中&#xff0c;在重视科研的同时&#xff0c;也不能忽略科研管理的重要性。做好教育科研的管理工作&#xff0c;可以有效提高科研工作的效率和质量。项目管理软件可以帮助教育科研团队更加高效地管理项目&#xff0c;并且简化团队成员之间的协作和沟通&#xff0…

【玩转Docker小鲸鱼叭】理解Docker的核心概念

Docker核心概念 Docker有三大核心概念&#xff1a;镜像&#xff08;Image&#xff09;、容器&#xff08;Container&#xff09;、仓库&#xff08;Repository&#xff09; 1、镜像&#xff08;Image&#xff09; Docker镜像 是我们创建和运行Docker容器的基础&#xff0c;它…

青大数据结构【2019】【三分析计算】

关键字: 邻接表时间复杂度、哈希表、平均查找长度ASL、堆排序 邻接表表示法 在邻接表上执行图的遍历操作时,需要对邻接表中所有的边(链表中的结点)访问一次,还需要对所有的顶点访问一次,故时间代价为O(n+2)。 1) 散列序号 0 1 2 3 4 5 6 7 元素 19 15 8 5 13 20

奉加微电子PhyPlusKit软件怎么使用

摘要&#xff1a;本文简介使用奉加微电子PhyPlusKit软件清除芯片、制作hexf文件、烧录程序、串口调试等操作方法。 所用硬件&#xff1a; PHY6222开发板&#xff0c;这个开发板上自带了CP210X串口芯片&#xff0c;与电脑的接口的type-c&#xff0c;既可以供电&#xff0c;又可…

探索Gradio Interface的强大功能与无限可能性——launch方法介绍

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

记录一个iOS头部放大计算

视图层级&#xff1a;由于这是在原有的视图层级的基础上完成的放大功能&#xff0c;所以记录了一下计算方法&#xff0c; tableview 和 放大的背景图片都是self.view的子视图&#xff0c;下拉的时候要方法&#xff0c;上滑的时候要同步上移图片 核心代码 [self.view addSubview…

回了一趟老家,我发现老家没有想象中那么舒服!

大家好&#xff0c;我是千与千寻&#xff0c;千寻最近回了一趟老家&#xff0c;说到回老家&#xff0c;我相信说应该大部人觉得是很舒服&#xff0c;自己很满意的生活节奏与感觉。 但是千寻在老家的这一个星期&#xff0c;感受到了非常多的不舒适&#xff0c;希望和星友们聊聊看…

三极管选型

来源网络&#xff0c;仅作笔记 三极管如何选型? 应根据电路的实际上需选取三极管的类别&#xff0c;即三极管在电路中的效用应与所选三极管的机能相吻合。 三极管的品种很多&#xff0c;分类的方式也不同&#xff0c;一般按半导体导电特点分成NPN型与PNP型两大类;按其在电路中…

zabbix-2-创建自定义监控项

例如监控iostat 下的sda tps值 [rootnode1 ly]# iostatLinux 3.10.0-1160.53.1.el7.x86_64 (node1) 2023年06月13日 _x86_64_ (32 CPU)avg-cpu: %user %nice %system %iowait %steal %idle0.06 0.00 0.04 0.01 0.00 99.89Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtnsda 1…

网工内推 | 金融业网工专场,员工旅游,带薪年假,节日福利

01 银信科技 招聘岗位&#xff1a;网络工程师 职责描述&#xff1a; 1&#xff09; 负责分支机构筹建网络系统调试与部署工作、网络运维管理及问题处理支持&#xff1b; 2&#xff09; 处理外部代理点系统及网络问题协助支持&#xff1b; 3&#xff09; 负责网络日志平台监控及…

PyCharm安装教程(图文结合,超详细,小白安装必看)

PyCharm安装教程(图文结合&#xff0c;超详细&#xff0c;小白安装必看) 一、Python开发环境 PyCharm集成开发工具&#xff08;IDE&#xff09;&#xff0c;是当下全球Python开发者&#xff0c;使用最频繁的工具软件。 绝大多数的Python程序&#xff0c;都是在PyCharm工具内…

python控制台学生管理系统

代码与注释 具体功能说明 设计初始界面设计学生信息录入 【数据校准】录入判断 学生姓名不能为空&#xff0c;并且不成超过4个字【数据校准】录入判断年龄在0-120 需要进行判断【数据校准】录入需要判断学号是否为空与学号是否在10位数【数据校准】录入需要判断成绩是否在0-1…

python数据分析-Mysql中NULL和‘ ‘怎么处理(不使用update)

一、空值NULL和空字符’ ’ 展示代码使用的版本是&#xff1a;8.0.28 空值NULL的长度是NULL&#xff0c;是占用存储空间的。空字符串’ 的长度是0&#xff0c;是不占用空间的。 理解&#xff1a;空字符串就像是一个真空状态的杯子&#xff0c;什么都没有;而空值NULL_就像是一…

17-事件循环(实现单线程非阻塞的方法就是事件循环)

一、是什么 &#x1f9c0;&#x1f9c0;&#x1f9c0;首先&#xff0c;JavaScript是一门单线程的语言&#xff0c;意味着同一时间内只能做一件事&#xff0c;但是这并不意味着单线程就是阻塞&#xff0c;而实现单线程非阻塞的方法就是事件循环 在JavaScript中&#xff0c;所有…