Эта статья — перевод оригинальной статьи «Essential Typescript for React».

Также я веду телеграм канал «Frontend по‑флотски», где рассказываю про интересные вещи из мира разработки интерфейсов.

Вступление

Typescript - сложный язык, но большинству разработчиков не нужно знать все его тонкости, чтобы быть эффективными в своей работе.

Это то, что я считаю минимальным набором знаний по тайпскрипту для эффективной разработки продукта на React.

Основные принципы, которые мы здесь используем:

  1. Типизируйте входные данные, предсказывай результат

  2. Минимизируйте шум в кодовой базе

  3. Ошибки должны отображаться как можно ближе к коду, который их вызвал

Используйте 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 /> в JSX

  • ReactNode - это 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], что более многословно и могут возникнуть трудности в случае внесения изменений в хук.

Комментарии (1)


  1. lrmpsm53
    22.11.2024 10:14

    По-моему это всё можно найти в доке реакта. Зачем эта статья?