Эта статья — перевод оригинальной статьи «Essential Typescript for React».
Также я веду телеграм канал «Frontend по‑флотски», где рассказываю про интересные вещи из мира разработки интерфейсов.
Вступление
Typescript - сложный язык, но большинству разработчиков не нужно знать все его тонкости, чтобы быть эффективными в своей работе.
Это то, что я считаю минимальным набором знаний по тайпскрипту для эффективной разработки продукта на React.
Основные принципы, которые мы здесь используем:
Типизируйте входные данные, предсказывай результат
Минимизируйте шум в кодовой базе
Ошибки должны отображаться как можно ближе к коду, который их вызвал
Используйте ReturnType и Awaited
Чтобы получить тип возвращаемой функции, используйте
ReturnType
.Чтобы получить возвращаемый тип асинхронной функции, оберните ее в
Awaited
.
export async function loader() {
return {…}
}
type LoaderData = Awaited<ReturnType<typeof loader>>
Типизируйте компоненты в соответствии с их требованиями
Когда вы перейдете к разработке с полным стеком безопасности типов, у вас появится возможность передавать типы непосредственно из базы данных в ваши компоненты. Обычно это является ошибкой, поскольку приводит к тому, что компоненты оказываются тесно связанными со схемой базы данных.
// неверно
function UserAddressCard({ user }: { user: User }) {
return (
<div>
<h1>{user.name}</h1>
<div>
{user.address.street}, {user.address.city}, {user.address.state},{" "}
{user.address.postalCode}
</div>
</div>
)
}
Этот компонент можно использовать только там, где у вас есть целый объект User
, что делает его гораздо менее переносимым, особенно если ему нужна только информация об адресе.
Также существует проблема с поддержкой. Поскольку этот компонент требует пользователя с адресом, а вы изменили схему базы данных так, что адрес стал необязательным, теперь существует несоответствие между тем, что хочет компонент, и тем, что дает база данных. Вы будете получать ошибки типа при каждом вызове user.address
.
Более подходящее место для получения ошибки - прямо там, где пользователь передается в качестве реквизита, в <UserAddressCard user={user} />
, чтобы мы могли решить ее, проверив наличие адреса и не прибегая к обработке условных случаев внутри карточки.
// правильно
function UserAddressCard({
user,
}: {
user: {
name: string
address: {
street: string
city: string
state: string
postalCode: string
}
}
})
Используйте ReactNode для типизации children
В React существует два способа типизации children:
ReactElement
- это тип, позволяющий вызывать функцию типа<Component />
в JSXReactNode
- этоReactElement
плюс все остальное, что может быть в JSX, например, строки, числа или null
Учитывая это, правильным способом ввода дочерних элементов компонента является использование ReactNode
.
// правильно
function Heading(props: { children: ReactNode }) {
return <h1 className="text-2xl font-bold">{props.children}</h1>
}
<Heading>Hello</Heading>
Если вместо этого вы используете ReactElement
, у вас будут ошибки при передаче не-элементов, и Typescript подтолкнет вас к тому, чтобы обернуть их в Fragments
, чтобы обойти это.
// неверно
function Heading(props: { children: ReactElement }) {
return <h1 className="text-2xl font-bold">{props.children}</h1>
}
<Heading>
<>Hello</>
</Heading>
? Если вы видите чрезмерное использование фрагментов, проверьте, не использовал ли кто-то ReactElement
вместо ReactNode
.
Используйте React.ComponentProps при передаче пропсов дочерним элементам.
Это очень полезный тип, особенно если вы увлекаетесь композицией. Если смотреть поверхностно, это выглядит вот так, и отлично подходит для случаев, когда нужно передать пропсы компоненту.
function Button(props: React.ComponentProps<"button">) {
return <button {...props} />
}
Это работает и для пользовательских компонентов с помощью оператора typeof
.
function PrimaryButton(props: React.ComponentProps<typeof Button>) {
return <Button variant="primary" {...props} />
}
? Если у вас есть пропсы, которые явно определены в типах, но не используются в компоненте, это, вероятно, кандидат на ComponentProps
.
// неправильно
function Button({
variant,
...props
}: {
variant: "primary"
// они не используются в явном виде
// Вероятно, их добавляли по одному, по мере того как они были нужны разработчику
type: string
className: string
}) {
return (
<button className={variant === "primary" ? "bg-blue-500" : ""} {...props} />
)
}
Используйте & для добавления дополнительных пропсов
function Button({
variant,
className,
...props
}: { variant: "primary" | "secondary" } & React.ComponentProps<"button">) {
return (
<button
{...props}
className={cn(
"bg-blue-500",
variant === "secondary" && "bg-gray-500",
className,
)}
/>
)
}
Используйте Omit, чтобы удалить пропсы, когда вы их переопределяете.
В приведенном выше примере пользователь все еще может передать Button
свое собственное имя класса, которое переопределит стили, применяемые вариантом. Это может быть хорошо для предоставления свободы разработчикам, но вы также можете использовать Omit
, чтобы отнять эту свободу.
function Button({
variant,
className,
...props
}: { variant: "primary" | "secondary" } & Omit<
React.ComponentProps<"button">,
"className"
>) {
return (
<button
{...props}
className={cn("bg-blue-500", variant === "secondary" && "bg-gray-500")}
/>
)
}
Один из сценариев, в котором это полезно, - переопределение существующего свойства.
Приведенный выше компонент Button
принимает className
как строку, но что, если вы хотите принимать массивы или что-то еще, что принимает функция cn
?
Простое объединение попытается объединить типы и в итоге получит never
.
// неверно
type ButtonProps = React.ComponentProps<"button"> & {
className: ClassValue | ClassValue[]
}
ButtonProps["className"] // never
Вместо этого используйте Omit
, чтобы удалить свойство className
из типа, а затем добавьте свой пользовательский тип с помощью&
.
type ButtonProps = Omit<React.ComponentProps<"button">, "className"> & {
className: ClassValue | ClassValue[]
}
Используйте объединения для групп связанных пропсов
С помощью объединений можно группировать связанные пропсы.
По умолчанию элемент button
имеет необязательный параметр type
, который по умолчанию принимает значение submit
.
Это может быть проблематично, потому что кнопка submit
вызывает побочные эффекты, если она находится внутри формы, а typescript не может проверить это.
С другой стороны, изменение значения по умолчанию на "button
" без побочных эффектов может запутать разработчиков, которые привыкли к тому, как это было последние 20 лет. Поэтому принуждение к явному объявлению - это компромисс, который я предпочитаю.
Еще больше усложняет ситуацию то, что иногда компонент Button
вообще не отображает элемент кнопки, как, например, в системе дизайна, где компонент используется для обертывания тега якоря. В этом случае тип бесполезен и может ввести разработчика в заблуждение, если он попытается передать его.
type ButtonProps = Omit<React.Component, "type"> &
(
| {
asChild: true
}
| {
asChild?: false
type: "button" | "submit" | "reset"
}
)
Это заставляет разработчика использовать хорошие комбинации пропсов
// правильно
<Button type="submit" />
<Button type="button" />
<Button asChild>
<Link href="/">Home</Link>
</Button>
// неправильно
<Button />
<Button asChild type="button">
<Link href="/">Home</Link>
</Button>
Используйте as const для кортежей
Чтобы использовать кортеж в хуке, как это делает useState
, вы можете использовать as const
, чтобы указать typescript, что это не массив
function useCopy() {
const [copied, setCopied] = useState()
return [
copied,
(text: string) => {
await navigator.clipboard.writeText(text)
setCopied(true)
},
] as const
}
? Обычный обходной путь - установить явный возвращаемый тип, например useCopy(): [boolean, (text: string) => Promise]
, что более многословно и могут возникнуть трудности в случае внесения изменений в хук.
lrmpsm53
По-моему это всё можно найти в доке реакта. Зачем эта статья?