TypeScript: как сделать интерфейс, в котором обязательно одно из двух свойств?
Решение через объединение типов (Union Types) с оператором never
Ваша задача — классический пример использования размеченных объединений (discriminated unions) в TypeScript. Основная идея: создать два отдельных интерфейса, каждый из которых содержит только одно из обязательных полей, а затем объединить их через | (union). Чтобы запретить наличие второго поля, используем вспомогательный тип Never.
Вот полное решение:
// Вспомогательный тип: запрещает наличие свойства
type Never<T> = {
[K in keyof T]?: never;
};
// Интерфейс для зарегистрированного пользователя
interface RegisteredUser {
name: string;
email: string;
userId: number;
guestToken?: never; // явно запрещаем guestToken
}
// Интерфейс для гостя
interface GuestUser {
name: string;
email: string;
userId?: never; // явно запрещаем userId
guestToken: string;
}
// Финальный тип: или зарегистрированный пользователь, или гость
type User = RegisteredUser | GuestUser;
Как это работает?
- Ключевое слово
neverв TypeScript означает "пустой тип" — значение, которого не может существовать. УказываяguestToken?: never, мы говорим: это свойство может быть толькоundefined(благодаря?), но если вы попытаетесь присвоить ему любое значение (кромеundefined), TypeScript выдаст ошибку. - Объединение
|создает тип, который может быть либоRegisteredUser, либоGuestUser. При проверке TypeScript будет требовать, чтобы объект соответствовал одному из этих интерфейсов.
Примеры использования
// ✅ Работает: зарегистрированный пользователь
const user1: User = {
name: "Анна",
email: "a@mail.ru",
userId: 123
};
// ✅ Работает: гость
const user2: User = {
name: "Иван",
email: "i@mail.ru",
guestToken: "abc123"
};
// ❌ Ошибка: нет обязательных полей
const user3: User = {
name: "Петр",
email: "p@mail.ru"
};
// Ошибка: Type '{ name: string; email: string; }' is not assignable to type 'User'.
// ❌ Ошибка: оба поля присутствуют
const user4: User = {
name: "Мария",
email: "m@mail.ru",
userId: 456,
guestToken: "xyz"
};
// Ошибка: Type '{ ... userId: number; guestToken: string; }' is not assignable to type 'User'.
Проверка внутри функции (type narrowing)
При работе с объединенными типами TypeScript помогает сужать тип через проверку наличия поля:
function processUser(user: User) {
if ("userId" in user) {
// Здесь TypeScript знает, что user — RegisteredUser
console.log(`Зарегистрированный пользователь с ID: ${user.userId}`);
} else {
// Здесь TypeScript знает, что user — GuestUser
console.log(`Гость с токеном: ${user.guestToken}`);
}
}
Альтернатива: общий интерфейс с never
Если вы предпочитаете один интерфейс, можно использовать такой трюк:
interface UserBase {
name: string;
email: string;
}
type User = (UserBase & { userId: number; guestToken?: never }) |
(UserBase & { userId?: never; guestToken: string });
Этот вариант более компактный, но менее читаемый. Я рекомендую первый подход с отдельными интерфейсами — он проще для понимания и поддержки.
<<Решение через условные типы (Conditional Types)
Другой подход — использовать обобщенный тип, который на основе флага выбирает нужную структуру. Этот метод полезен, если у вас много вариантов или нужно переиспользовать логику.
// Определяем возможные варианты
type UserType = 'registered' | 'guest';
// Условный тип, который выбирает поля в зависимости от типа
type User<T extends UserType> = {
name: string;
email: string;
} & (T extends 'registered'
? { userId: number; guestToken?: never }
: { userId?: never; guestToken: string });
// Использование с явным указанием типа
const user1: User<'registered'> = {
name: "Анна",
email: "a@mail.ru",
userId: 123
};
const user2: User<'guest'> = {
name: "Иван",
email: "i@mail.ru",
guestToken: "abc"
};
Плюсы и минусы
- + Явное указание типа делает код самодокументируемым.
- + Легко добавить третий вариант (например,
'admin'). - - Нужно всегда передавать тип при создании объекта.
- - Менее удобно для функций, которые принимают «любого пользователя».
Если вам нужно больше гибкости, комбинируйте этот подход с дискриминантным объединением (добавьте поле type в интерфейс). Это позволит TypeScript автоматически сужать тип при проверке значения type.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги