Avatar ·

Как извлечь тип свойства интерфейса TypeScript?

Всем привет! Я новичок в TypeScript и столкнулся с проблемой, которую не могу решить уже несколько часов. У меня есть интерфейс, описывающий пользователя: ```typescript interface User { id: number; name: string; email: string; role: 'admin' | 'user' | 'guest'; createdAt: Date; } ``` Теперь мне нужно создать отдельный тип, который будет соответствовать только свойству `role` из этого интерфейса. То есть я хочу получить тип `'admin' | 'user' | 'guest'` без необходимости повторно объявлять его вручную. Я пробовал писать что-то вроде `type RoleType = User['role']`, но не уверен, что это правильный синтаксис и вообще возможно ли такое в TypeScript. Пытался гуглить, но нашел много сложных примеров с условными типами и mapped types, которые пока не понимаю. Подскажите, есть ли простой и элегантный способ "вытащить" тип конкретного свойства из интерфейса? Буду благодарен за примеры кода и объяснение для новичков. Теги: typescript, typing, интерфейсы, свойства <<>>

Да, это возможно! Используйте индексированный доступ к типу (Indexed Access Types)

Ваша догадка абсолютно верна. В TypeScript есть встроенный механизм для извлечения типа свойства интерфейса — это синтаксис с квадратными скобками, аналогичный доступу к свойствам объекта в JavaScript. Работает это просто и элегантно:

type RoleType = User['role'];
// Результат: 'admin' | 'user' | 'guest'

Это называется индексированный доступ к типу (indexed access type). TypeScript автоматически выводит тип свойства role из интерфейса User и создаёт новый тип, который можно использовать где угодно.

Почему это работает?

В TypeScript интерфейс — это не просто описание, а полноценный тип. Когда вы пишете User['role'], компилятор "заглядывает" внутрь интерфейса и возвращает тип, который соответствует указанному ключу. Это работает для любых свойств:

type UserIdType = User['id'];        // number
type UserNameType = User['name'];     // string
type UserDateType = User['createdAt']; // Date

Продвинутые примеры использования

  • Извлечение нескольких свойств: если нужен тип для нескольких свойств, используйте union ключей:
    type NameAndEmail = User['name' | 'email']; // string
  • Извлечение всех значений: можно получить union всех значений интерфейса:
    type AllUserValues = User[keyof User]; 
    // number | string | Date | 'admin' | 'user' | 'guest'
  • Работа с вложенными объектами: если свойство само является объектом, можно извлечь его вложенные типы:
    interface User {
      profile: {
        firstName: string;
        lastName: string;
        age: number;
      };
    }
    
    type ProfileFirstName = User['profile']['firstName']; // string
  • Создание обобщённых утилит: можно написать функцию, которая принимает объект и возвращает тип его свойства:
    function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
      return obj[key];
    }
    
    const user: User = { /* ... */ };
    const role = getProperty(user, 'role'); // тип: 'admin' | 'user' | 'guest'

Когда это может быть полезно?

  • При работе с Redux или другими state-менеджерами, где нужно строго типизировать отдельные части состояния.
  • При создании компонентов, принимающих только часть данных из интерфейса.
  • При валидации форм, где типы полей должны соответствовать типам в интерфейсе.
  • При написании утилит для работы с API, где ответ сервера имеет сложную вложенную структуру.

Этот подход полностью типобезопасен: если вы измените тип свойства в интерфейсе, TypeScript автоматически обновит все типы, которые от него зависят. Никакой дупликации кода!

<<>>

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

Если вам нужно извлечь тип свойства из сложного типа или объединения типов, можно использовать условные типы с ключевым словом infer:

type ExtractPropertyType<T, K> = T extends { [P in K]: infer R } ? R : never;

type RoleType = ExtractPropertyType<User, 'role'>;
// 'admin' | 'user' | 'guest'

Этот подход особенно полезен, когда вы работаете с объединениями типов или не знаете точную структуру объекта на момент написания кода. Например:

type Shape = 
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; side: number }
  | { kind: 'triangle'; base: number; height: number };

type ShapeKind = ExtractPropertyType<Shape, 'kind'>;
// 'circle' | 'square' | 'triangle'

Однако для большинства повседневных задач достаточно простого индексированного доступа User['role']. Условные типы стоит использовать только тогда, когда стандартный подход не работает из-за сложности типов.