你需要了解的 TypeScript 实用程序类型
您是否曾经使用 TypeScript 构建过某些东西并意识到……
啊!这个包导出的不是我需要的类型!
幸运的是,TypeScript 为我们提供了许多可以解决这个常见问题的实用类型。
例如,要获取函数返回的类型,我们可以使用该ReturnType
实用程序:
import { getContent } from '@builder.io'
const content = await getContent()
// 😍
type Content = ReturnType<typeof getContent>
但是我们有一个小问题。getContent
是一个async
返回承诺的函数,所以目前我们的Content
类型实际上是Promise<Content>
,这不是我们想要的。
为此,我们可以使用该Awaited
类型来解开承诺并获取承诺解析的类型:
import { getContent } from '@builder.io'
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
排除任何null
或值。undefined
// ✅
type ContentKind = NonNullable<Parameters<typeof getContent>[0]>
// ContentKind
现在我们的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) {
// ...
}
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 不会意识到状态的变化,因此不会重新渲染。
// 🚩
event.title = e.target.value
我们需要做的是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
}}
/>
)
}
现在,当我们更新代码以根据需要复制事件时,TypeScript 再次感到高兴:
<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>>()
// ...
// 🚩 No warnings from TypeScript, even though this is a bug
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]>
}
感谢 Dean Merchant 提供上述代码片段。
现在,使用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 })
}}
/>
)
}
这使得我们只需提供已更改的属性,并且可以自动管理复制以获得良好的 DX 和安全保障。
但是我们遇到了一个新问题。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>) { /* ... */ }
// ...
// All clear!
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">>'
更多公用事业
我们在这里介绍了不少 TypeScript 实用程序!这里只简单介绍一下剩下的几个,在我看来,它们都非常有用。
记录<KeyType, ValueType>
创建一个表示具有任意键且具有给定类型的值的对象类型的简单方法:
const months = Record<string, number> = {
january: 1,
february: 2,
march: 3,
// ...
}
排除<UnionType, ExcludedMembers>
从联合中删除所有可分配给该ExcludeMembers
类型的成员。
type Months = 'january' | 'february' | 'march' | // ...
type MonthsWith31Days = Exclude<Months, 'april' | 'june' | 'september' | 'november'>
// 'january' | 'february' | 'march' | 'may' ...
摘录<Union, Type>
从联合中删除所有不可分配给的成员Type
。
type Extracted = Extract<string | number, (() => void), Function>
// () => void
ConstructorParameters<Type>
就像参数一样,但是对于构造函数来说:
class Event {
constructor(title: string, date: Date) { /* ... */ }
}
type EventArgs = ConstructorParameters<Event>
// [string, Date]
InstanceType<Type>
为您提供构造函数的实例类型。
class Event { ... }
type Event = InstaneType<typeof Event>
// Event
ThisParameterType<Type>
为您提供函数参数的类型this
,如果未提供则为未知。
function getTitle(this: Event) { /* ... */ }
type This = ThisType<typeof getTitle>
// Event
OmitThisParameter<Type>
this
从函数类型中删除参数。
function getTitle(this: Event) { /* ... */ }
const getTitleOfMyEvent: OmitThisParameter<typeof getTitle> =
getTitle.bind(myEvent)
结论
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)
给你这个:
鏂囩珷鏉ユ簮锛�https://dev.to/builderio/typescript-utility-types-you-need-to-know-14b7