你是否曾经用 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
是一个返回 promise
的 async
函数,所以目前我们的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)