Avatar ·

TypeScript: Ошибка "Type 'string' is not assignable to type" при работе с объектами

Привет! Я начал изучать TypeScript и столкнулся со странной ошибкой. У меня есть интерфейс, описывающий пользователя: ```typescript interface User { name: string; age: number; role: 'admin' | 'user'; } ``` Я пытаюсь создать функцию, которая принимает строку с ролью и возвращает объект пользователя: ```typescript function createUser(name: string, age: number, role: string): User { return { name, age, role }; } ``` Но TypeScript выдаёт ошибку: "Type 'string' is not assignable to type '"admin" | "user"'" в месте `role`. Я не понимаю, почему это происходит. Ведь `role` — это строка, и я передаю строку, которая должна подходить под тип `'admin' | 'user'`. В JavaScript это работало бы без проблем. Я пробовал явно привести тип: `role as 'admin' | 'user'`, но это кажется костылём. Есть ли правильный способ решить эту проблему? Может, я неправильно проектирую интерфейс? Буду благодарен за подробное объяснение. <<>>

Причина ошибки: TypeScript защищает вас от неявных нарушений типов

Ошибка возникает из-за того, что TypeScript не может гарантировать, что любая строка, переданная в параметр role: string, будет соответствовать литеральному объединению 'admin' | 'user'. Даже если вы на 100% уверены, что передадите только правильные значения, компилятор проверяет статические типы, а не runtime-значения.

Правильное решение: используйте литеральный тип в параметре

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

interface User {
  name: string;
  age: number;
  role: 'admin' | 'user';
}

// ✅ Правильно: параметр role имеет тот же тип, что и в интерфейсе
function createUser(name: string, age: number, role: 'admin' | 'user'): User {
  return { name, age, role };
}

// Использование:
const user1 = createUser('Alice', 30, 'admin'); // ✅ OK
const user2 = createUser('Bob', 25, 'user');    // ✅ OK
// const user3 = createUser('Eve', 20, 'moderator'); // ❌ Ошибка на этапе компиляции

Альтернативный подход: создайте отдельный тип для роли

Если тип роли используется в нескольких местах, вынесите его в отдельный тип для переиспользования:

type UserRole = 'admin' | 'user';

interface User {
  name: string;
  age: number;
  role: UserRole;
}

function createUser(name: string, age: number, role: UserRole): User {
  return { name, age, role };
}

Когда всё-таки нужно использовать приведение типа (as)

Приведение типа (as) — это не "костыль", а осознанный инструмент, который стоит применять в следующих ситуациях:

  • Данные из внешних источников (API, localStorage, форм). Вы уверены в данных, но TypeScript не может это проверить:
function parseUserRole(data: string): User | null {
  // Проверка в runtime
  if (data === 'admin' || data === 'user') {
    return { name: 'Unknown', age: 0, role: data as UserRole }; // ✅ безопасное приведение
  }
  return null;
}
  • Динамические вычисления, когда результат гарантированно корректен:
const roles = ['admin', 'user'] as const;
function getRole(index: number): UserRole {
  return roles[index] as UserRole; // ✅ мы знаем, что index будет 0 или 1
}

Дополнительная рекомендация: используйте const assertions для массивов

Если у вас есть массив допустимых ролей, используйте as const для вывода точных литеральных типов:

const VALID_ROLES = ['admin', 'user'] as const;
type UserRole = typeof VALID_ROLES[number]; // 'admin' | 'user'

function createUser(name: string, age: number, role: UserRole): User {
  return { name, age, role };
}

Этот подход даёт максимальную типобезопасность: если вы измените массив VALID_ROLES, типы автоматически обновятся.

Вывод

Всегда старайтесь использовать точные типы в параметрах функций вместо string, если набор значений ограничен. Это делает код самодокументируемым и предотвращает ошибки на этапе компиляции. Приведение типа (as) используйте только когда уверены в данных, но TypeScript не может это проверить статически.

<<>>

Решение через discriminated union и type guards

Если ваша функция должна принимать именно string (например, из пользовательского ввода), используйте type guard для проверки в runtime:

type UserRole = 'admin' | 'user';

function isValidRole(role: string): role is UserRole {
  return role === 'admin' || role === 'user';
}

function createUser(name: string, age: number, role: string): User | null {
  if (!isValidRole(role)) {
    console.error(`Invalid role: ${role}`);
    return null; // или выбросить ошибку
  }
  return { name, age, role }; // ✅ TypeScript понимает, что role теперь UserRole
}

Преимущества:

  • Безопасная обработка непредсказуемых входных данных
  • Явная валидация с понятной ошибкой
  • TypeScript автоматически сужает тип после проверки

Недостатки:

  • Функция может возвращать null, что требует дополнительной обработки
  • Больше кода для простых случаев

Этот подход особенно полезен, когда вы работаете с данными из форм, API запросов или любых других источников, где тип string — единственная гарантия.