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

你是否曾经用 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)

猜你喜欢

转载自blog.csdn.net/ImagineCode/article/details/131280104