Размеченные объединения для вариантов компонентов
Моделируйте свойства вариантов с помощью размеченных объединений, чтобы TypeScript проверял допустимые сочетания свойств.
«Размеченные объединения для вариантов компонентов» — бесплатный урок React Academy на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения React Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс React Academy содержит 4 уроков всего.
Проблема свойств вариантов
У компонента с необязательными свойствами для разных режимов (например, у кнопки, которая может быть ссылкой или кнопкой) могут возникать недопустимые сочетания свойств. TypeScript не может обнаружить их без дискриминируемых объединений.
Что такое дискриминируемое объединение?
Дискриминируемое объединение — это объединение типов с общим полем литерального типа (дискриминатором). TypeScript сужает тип на основе значения этого поля.
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number };
function area(shape: Shape): number {
if (shape.kind === 'circle') return Math.PI * shape.radius ** 2;
return shape.width * shape.height; // TS knows width/height exist here
}Вариант Button и ссылки
Опишите полиморфный компонент с дискриминируемым объединением по полю as или variant, чтобы для каждого случая обеспечить правильные свойства.
type ButtonProps =
| { as: 'button'; onClick: () => void; disabled?: boolean; children: React.ReactNode }
| { as: 'a'; href: string; target?: string; children: React.ReactNode };
function ActionButton(props: ButtonProps) {
if (props.as === 'button') {
return <button onClick={props.onClick} disabled={props.disabled}>{props.children}</button>;
}
return <a href={props.href} target={props.target}>{props.children}</a>;
}Варианты компонента оповещения
Опишите компонент оповещения с разными обязательными данными для каждого типа серьёзности.
type AlertProps =
| { type: 'success'; message: string }
| { type: 'error'; message: string; onRetry: () => void }
| { type: 'warning'; message: string; details?: string };
function Alert(props: AlertProps) {
if (props.type === 'error') {
return (
<div className="alert error">
<p>{props.message}</p>
<button onClick={props.onRetry}>Retry</button>
</div>
);
}
return <div className={`alert ${props.type}`}>{props.message}</div>;
}Сужение типов в обработчиках событий
Дискриминируемые объединения работают и с данными, управляемыми событиями, — это особенно удобно для автоматов состояний или состояния на основе действий.
type LoadingState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function DataView<T>({ state }: { state: LoadingState<T> }) {
if (state.status === 'loading') return <Spinner />;
if (state.status === 'error') return <p>{state.error.message}</p>;
if (state.status === 'success') return <pre>{JSON.stringify(state.data)}</pre>;
return null;
}Исчерпывающие проверки с never
Добавьте проверку never в ветвь по умолчанию, чтобы получить ошибку TypeScript, если добавлен новый элемент объединения, но для него не предусмотрена обработка.
function assertNever(x: never): never {
throw new Error('Unhandled case: ' + x);
}
function renderIcon(type: AlertProps['type']) {
switch (type) {
case 'success': return <CheckIcon />;
case 'error': return <XIcon />;
case 'warning': return <WarnIcon />;
default: return assertNever(type); // TS error if a case is missing
}
}Дискриминируемые объединения для ответов API
Моделируйте формы ответов API как дискриминируемые объединения, чтобы вызывающий код обрабатывал успешные и ошибочные пути без приведения типов.
type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: string; code: number };
async function fetchUser(id: string): Promise<ApiResult<User>> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) return { ok: false, error: 'Not found', code: res.status };
return { ok: true, data: await res.json() };
}Защиты типов для сужения
Используйте пользовательские защиты типов, чтобы сужать объединения в более сложных случаях, когда дискриминатор нельзя проверить простым сравнением.
function isSuccess<T>(result: ApiResult<T>): result is { ok: true; data: T } {
return result.ok === true;
}
const result = await fetchUser('1');
if (isSuccess(result)) {
console.log(result.data.name); // TS knows data exists
}Как избежать скопления необязательных свойств
Без дискриминируемых объединений компоненты обрастают необязательными свойствами, допустимыми только в определённых сочетаниях, — это запутывает и не проверяется типами. Дискриминируемые объединения исключают невозможные состояния.
// Bad: optional prop soup — invalid combos allowed:
interface BadProps {
href?: string;
onClick?: () => void;
disabled?: boolean;
}
// Good: only valid combos via discriminated union:
type GoodProps =
| { as: 'a'; href: string }
| { as: 'button'; onClick: () => void; disabled?: boolean };Композиция объединений
Используйте & (пересечение), чтобы добавить общие свойства всем элементам объединения.
type BaseProps = { className?: string; children: React.ReactNode };
type ButtonVariant =
| (BaseProps & { variant: 'primary'; onClick: () => void })
| (BaseProps & { variant: 'link'; href: string });Дискриминация во время выполнения
React использует дискриминатор во время выполнения, чтобы отрисовать правильный интерфейс. TypeScript использует его во время компиляции, чтобы обеспечить корректное использование свойств. Защищены оба уровня.
Быстрая проверка
Что является дискриминатором в дискриминируемом объединении?
Итоги
Дискриминируемые объединения моделируют компоненты с взаимоисключающими наборами свойств, используя общее поле с литеральным типом в качестве дискриминатора. Используйте never в ветви по умолчанию для исчерпывающих проверок. Такие объединения исключают недопустимые сочетания свойств, разрешаемые необязательными свойствами, и делают компоненты самодокументируемыми и типобезопасными.
Часто задаваемые вопросы
Урок «Размеченные объединения для вариантов компонентов» бесплатный?
Да — полный текст урока «Размеченные объединения для вариантов компонентов» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс React Academy, подпишись на CoddyKit PRO. Курс React Academy содержит 4 уроков всего.
Чему я научусь в уроке «Размеченные объединения для вариантов компонентов»?
Моделируйте свойства вариантов с помощью размеченных объединений, чтобы TypeScript проверял допустимые сочетания свойств. Ты практикуешь React Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать React Academy?
Предыдущий опыт не требуется. React Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.
Сколько времени занимает урок «Размеченные объединения для вариантов компонентов»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке React Academy?
Да. Каждый урок React Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Размеченные объединения для вариантов компонентов
- Условные и отображаемые типы в React
- Полиморфные компоненты со свойством 'as'
- Типобезопасные формы и контракты ответов API