Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Як через дискриміновані union-типи зробити неможливими недопустимі комбінації props?

Звичайні необов'язкові props дозволяють будь-які комбінації, навіть безглузді:

type ButtonProps = {
  href?: string;          // посилання
  onClick?: () => void;   // кнопка
  external?: boolean;     // має сенс лише з href
};

<Button onClick={save} external />   // компілюється, хоча безглуздо
<Button />                            // ні дії, ні посилання

Дискримінований union описує кожен допустимий варіант окремо:

type LinkProps = { as: 'link'; href: string; external?: boolean };
type ActionProps = { as: 'button'; onClick: () => void; disabled?: boolean };

type ButtonProps = (LinkProps | ActionProps) & { children: ReactNode };

function Button(props: ButtonProps) {
  if (props.as === 'link') {
    return (
      <a href={props.href} target={props.external ? '_blank' : undefined}>
        {props.children}
      </a>
    );
  }
  return (
    <button onClick={props.onClick} disabled={props.disabled}>
      {props.children}
    </button>
  );
}
<Button as="link" href="/jobs">Вакансії</Button>
<Button as="button" onClick={save}>Зберегти</Button>
<Button as="button" href="/x">...</Button>   // помилка: href не існує для button

Варіант без явного дискримінатора - через never, щоб props взаємно виключали один одного:

type Props =
  | { value: string; defaultValue?: never; onChange: (v: string) => void }   // керований
  | { defaultValue?: string; value?: never; onChange?: never };              // некерований

Передати одночасно value і defaultValue неможливо - типова помилка з полями форм.

Типові застосування:

  • керований / некерований компонент;
  • компонент стану запиту: { status: 'loading' } | { status: 'error'; error: string } | { status: 'success'; data: T };
  • модальне вікно з різними наборами кнопок;
  • поле форми, де options обов'язкові лише для типу select.

Пастки:

  • деструктуризація в параметрах губить звуження: function Button({ as, href, onClick }) - href не існує у варіанті button, TypeScript не дасть деструктуризувати. Звужувати треба по props.as, а деструктуризувати вже всередині гілки;
  • ...rest з union дає перетин полів і може «загубити» варіант - передаючи props далі, краще явно перелічувати;
  • повідомлення про помилки для великих union стають довгими - варто тримати варіантів небагато й давати їм імена.

Докладніше в документації: Звуження типів: дискриміновані union

Перевір себе

20 випадкових питань за спробу, після завершення - розбір кожної помилки

Схожі питання