前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Typescript 中,这些类型工具真好用

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

作者头像
前端修罗场
发布2023-10-07 19:58:02
1770
发布2023-10-07 19:58:02
举报
文章被收录于专栏:Web 技术Web 技术

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

在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
import {getContent, Content} from '@example'
const content = await getContent()
function processContent(content:Content) {
 // ...
}

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

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

代码语言:javascript
复制
import { getContent } from '@example'
const content = await getContent()

type Content = ReturnType<typeof getContent>

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

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

代码语言:javascript
复制
import { getContent } from '@example'
const content = await getContent()

type Content = Awaited<ReturnType<typeof getContent>>

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

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

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

代码语言:javascript
复制
type Arguments = Parameters<typeof getContent>
// [ContentKind | ?developer/article/2336000/undefined]

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

代码语言:javascript
复制
type ContentKind = Parameters<typeof getContent>[0]

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

代码语言:javascript
复制
type ContentKind = NonNullable<Parameters<typeof getContent>[0]>
// ContentKind

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

代码语言:javascript
复制
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 组件方面给我们很大的帮助。

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

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

代码语言:javascript
复制
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 类型工具来提高类型安全性,以强制我们不应该改变该对象的任何属性:

代码语言:javascript
复制
const [event, setEvent] = useState<Readonly<Event>>()

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

代码语言:javascript
复制
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
      }}
    />
  )
}

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

代码语言:javascript
复制
<input
  placeholder="Event title"
  value={event.title} 
  onChange={e => {
    // ?
    setState({ ...event, title: e.target.value })
  }}
/>

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

代码语言:javascript
复制
export function EditEvent() {
  const [event, setEvent] = useState<Readonly<Event>>()
  // ...

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

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

代码语言:javascript
复制
export type DeepReadonly<T> =
  T extends Primitive ? T :
  T extends Array<infer U> ? DeepReadonlyArray<U> :
  DeepReadonlyObject<T>

type Primitive = 
  string | number | boolean | ?developer/article/2336000/undefined | null

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

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

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

代码语言:javascript
复制
export function EditEvent() {
  const [event, setEvent] = useState<DeepReadonly<Event>>()
  // ...

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

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

代码语言:javascript
复制
export function EditEvent() {
  const [event, setEvent] = useState<DeepReadonly<Event>>()
  
  // ...
  
  // ?
  setEvent({
    ...event,
    title: e.target.value,
    attendees: [...event.attendees, 'foo']
  })
}

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

代码语言:javascript
复制
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 期望得到完整的事件对象,但是我们想要的只是一个部分对象,所以我们会得到下面这样的错误:

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

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

代码语言:javascript
复制
// ?
function updateEvent(newEvent: Partial<Event>) { /* ... */ }
// ...
updateEvent({ title: e.target.value })

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

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

代码语言:javascript
复制
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 来省略指定的键:

代码语言:javascript
复制
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>

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

代码语言:javascript
复制
const months = Record<string, number> = {
  january: 1,
  february: 2,
  march: 3,
  // ...
}

Exclude<UnionType, ExcludedMembers>

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

代码语言:javascript
复制
type Months = 'january' | 'february' | 'march' | // ...
type MonthsWith31Days = Exclude<Months, 'april' | 'june' | 'september' | 'november'>
// 'january' | 'february' | 'march' | 'may' ...

Extract<Union, Type>

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

代码语言:javascript
复制
type Extracted = Extract<string | number, (() => void), Function>
// () => void

ConstructorParameters

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

代码语言:javascript
复制
class Event {
  constructor(title: string, date: Date) { /* ... */ }
}
type EventArgs = ConstructorParameters<Event>
// [string, Date]

InstanceType

给出构造函数的 instance 类型:

代码语言:javascript
复制
class Event { ... }
type Event = InstanceType<typeof Event>
// Event

ThisParameterType

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

代码语言:javascript
复制
function getTitle(this: Event) { /* ... */ }
type This = ThisType<typeof getTitle>
// Event

OmitThisParameter

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

代码语言:javascript
复制
function getTitle(this: Event) { /* ... */ }
const getTitleOfMyEvent: OmitThisParameter<typeof getTitle> = 
  getTitle.bind(myEvent)
本文参与?腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客?前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 在 React 中使用工具类型
  • 更多类型工具
    • Record<KeyType, ValueType>
      • Exclude<UnionType, ExcludedMembers>
        • Extract<Union, Type>
          • ConstructorParameters
            • InstanceType
              • ThisParameterType
                • OmitThisParameter
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
                http://www.vxiaotou.com