你需要了解的 TypeScript 实用程序类型

2025-06-11

你需要了解的 TypeScript 实用程序类型

您是否曾经使用 TypeScript 构建过某些东西并意识到……

需要未从包中导出的类型的代码的屏幕截图

啊!这个包导出的不是我需要的类型!

幸运的是,TypeScript 为我们提供了许多可以解决这个常见问题的实用类型。

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

import { getContent } from '@builder.io'
const content = await getContent()
// 😍
type Content = ReturnType<typeof getContent>
Enter fullscreen mode Exit fullscreen mode

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

为此,我们可以使用该Awaited类型来解开承诺并获取承诺解析的类型:

import { getContent } from '@builder.io'
const content = await getContent()
// ✅
type Content = Awaited<ReturnType<typeof getContent>>
Enter fullscreen mode Exit fullscreen mode

现在,即使没有显式导出,我们也确实找到了所需的类型。嗯,这下松了一口气。

但是如果我们需要该函数的参数类型怎么办?

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

type Arguments = Parameters<typeof getContent>
// [ContentKind | undefined]
Enter fullscreen mode Exit fullscreen mode

Parameters为您提供一个参数类型的元组,您可以通过索引提取特定的参数类型,如下所示:

type ContentKind = Parameters<typeof getContent>[0]
Enter fullscreen mode Exit fullscreen mode

但还有最后一个问题。因为这是一个可选参数,所以我们ContentKind现在的类型实际上是ContentKind | undefined,这不是我们想要的。

为此,我们可以使用实用程序类型,从联合类型中NonNullable排除任何null或值。undefined

// ✅
type ContentKind = NonNullable<Parameters<typeof getContent>[0]>
// ContentKind
Enter fullscreen mode Exit fullscreen mode

现在我们的ContentKind类型ContentKind与这个包中未导出的类型完全匹配,我们可以在我们的processContent函数中使用它,如下所示:

import { getContent } from '@builder.io'

const content = await getContent()

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

// 🥳
function processContent(content: Content, kind: ContentKind) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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
      }}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

哦,我们正在直接改变事件对象。

这将导致我们的输入无法按预期工作,因为 React 不会意识到状态的变化,因此不会重新渲染。

// 🚩
event.title = e.target.value
Enter fullscreen mode Exit fullscreen mode

我们需要做的是setEvent使用新对象进行调用。

但是等等,为什么 TypeScript 没有捕获到这个?

好吧,从技术上讲,可以用 来改变对象useState。但基本上你永远不应该这么做。我们可以通过使用实用程序类型来提高类型安全性Readonly,强制我们不应该改变此对象的任何属性:

// ✅
const [event, setEvent] = useState<Readonly<Event>>()
Enter fullscreen mode Exit fullscreen mode

现在我们之前的错误将被自动捕获,哇!

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
      }}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

现在,当我们更新代码以根据需要复制事件时,TypeScript 再次感到高兴:

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

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

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

  // 🚩 No warnings from TypeScript, even though this is a bug
  event.attendees.push('foo')
}
Enter fullscreen mode Exit fullscreen mode

但是,既然我们已经知道了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]>
}
Enter fullscreen mode Exit fullscreen mode

感谢 Dean Merchant 提供上述代码片段

现在,使用DeepReadonly,我们无法改变整个树中的任何内容,从而防止可能发生的一系列错误。

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

  event.attendees.push('foo')
  //             ^^^^ Error!
}
Enter fullscreen mode Exit fullscreen mode

仅当正确且不可变地处理时,才会通过类型检查:

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

  // ...

  // ✅
  setEvent({
    ...event,
    title: e.target.value,
    attendees: [...event.attendees, 'foo']
  })
}
Enter fullscreen mode Exit fullscreen mode

对于这种复杂性,您可能想要使用的另一个模式是将此逻辑移动到自定义钩子,我们可以这样做:

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 })
      }}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

这使得我们只需提供已更改的属性,并且可以自动管理复制以获得良好的 DX 和安全保障。

但是我们遇到了一个新问题。updateEvent需要完整的事件对象,但我们想要的只是一个部分对象,所以我们得到以下错误:

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

幸运的是,这个问题可以通过实用程序类型轻松解决Partial,它使所有属性都成为可选的:

// ✅
function updateEvent(newEvent: Partial<Event>) { /* ... */ }
// ...
// All clear!
updateEvent({ title: e.target.value })
Enter fullscreen mode Exit fullscreen mode

除此之外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">>'
Enter fullscreen mode Exit fullscreen mode

或者类似地,我们可以用来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">>'
Enter fullscreen mode Exit fullscreen mode

更多公用事业

我们在这里介绍了不少 TypeScript 实用程序!这里只简单介绍一下剩下的几个,在我看来,它们都非常有用。

记录<KeyType, ValueType>

创建一个表示具有任意键且具有给定类型的值的对象类型的简单方法:

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

排除<UnionType, ExcludedMembers>

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

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

摘录<Union, Type>

从联合中删除所有不可分配给的成员Type

type Extracted = Extract<string | number, (() => void), Function>
// () => void
Enter fullscreen mode Exit fullscreen mode

ConstructorParameters<Type>

就像参数一样,但是对于构造函数来说:

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

InstanceType<Type>

为您提供构造函数的实例类型。

class Event { ... }
type Event = InstaneType<typeof Event>
// Event
Enter fullscreen mode Exit fullscreen mode

ThisParameterType<Type>

为您提供函数参数的类型this,如果未提供则为未知。

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

OmitThisParameter<Type>

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

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

结论

TypesScript 中的实用类型很有用。使用它们吧。

关于我

大家好!我是 Builder.io的 CEO  Steve

我们通过拖放组件的方式在您的网站或应用程序上以可视化的方式创建页面和其他 CMS 内容。

所以这样:

import { BuilderComponent, registerComponent } from '@builder.io/react'
import { Hero, Products } from './my-components'

// Dynamically render compositions of your components
export function MyPage({ json }) {
  return <BuilderComponent content={json} />
}

// Use your components in the drag and drop editor
registerComponent(Hero)
registerComponent(Products)
Enter fullscreen mode Exit fullscreen mode

给你这个:

Builder.io 的 Gif

鏂囩珷鏉ユ簮锛�https://dev.to/builderio/typescript-utility-types-you-need-to-know-14b7
PREV
使用 Python 的人脸检测技术来解决这个问题
NEXT
Bun 与 Node.js:你需要知道的一切