Юнион лучше опциональных свойств

Это заметка про тайпскрипт, и опытные разработчики не узнают что-то новое, но зато вот URL, на который можно ссылаться.

Скажем, у нас есть интерфейс, который используется и для ошибок, и для предупреждений:

interface NotificationProps {
  type: 'error' | 'warning'
  message: string
}

И добавляется новое требование, что в нотификашке предупреждений должна быть кнопка «Исправить» с настраиваемым текстом.

Наивный способ — расширить интерфейс опциональными свойствами:

interface NotificationProps {
  type: 'error' | 'warning'
  message: string
  buttonText?: string
  buttonAction?: () => void
}

Это меньше кода менять, интерфейс уже объявлен, просто докинули свойство и заиспользовали там, где показываем предупреждения, да? Нет.

У интерфейса «до» было всего два валидных варианта:

  1. { type: "error", message }
  2. { type: "warning", message }

Добавив два опциональных свойства, мы создали 8 возможных комбинаций:

  1. error + buttonText + buttonAction
  2. error + no buttonText + buttonAction
  3. error + buttonText + no buttonAction
  4. error + no buttonText + no buttonAction
  5. warning + buttonText + buttonAction
  6. warning + no buttonText + buttonAction
  7. warning + buttonText + no buttonAction
  8. warning + no buttonText + no buttonAction

Каждое опциональное свойство удваивает количество разрешенных состояний.

И большая часть этих состояний нарушает требования.

Вполне возможно, что конкретный код, который есть в кодовой базе сейчас, не попадает в неправильные состояния! Но это не гарантирует, что такого не произойдёт в будущем. Мы держим в голове, какие состояния правильные, а какие нет, и тайпскрипт нам не сможет помочь — это не очень надёжно.

Решение

Вариант получше с точки зрения долгосрочной надёжности: использовать юнионы, чтобы описать два конкретных правильных состояния:

type NotificationProps = { type: 'error', message: string } | {
  type: 'warning',
  message: string,
  buttonText: string,
  buttonAction: () => void
}

Можно даже вынести общую часть, но пока кажется оверкиллом:

type NotificationProps = { message: string } & (
 | { type: 'error' }
 | { type: 'warning', buttonText: string, buttonAction: () => void }
)

TS Playground поиграть.

Published at