Собеседования

Дженерики TypeScript на собеседовании: extends, keyof, infer и свои утилиты

Дженерики на задачах с собеседований: ограничения через extends, keyof и доступ по ключу, условные типы с infer и свои Pick, Partial и ReturnType.

Дженерики - тема, на которой видно, понимает ли кандидат систему типов или только расставляет аннотации. На собеседовании обычно просят объяснить идею, типизировать небольшую функцию и написать свою версию стандартной утилиты. Пройдём этот путь по шагам.

Идея: параметр типа

Функция first(items) может вернуть any, и тогда вызывающий код потеряет тип. Дженерик позволяет сказать: «какой тип элементов пришёл, такой и вернётся». TypeScript выводит T из аргумента, писать его явно обычно не нужно.

TSX// Параметр типа связывает вход и выход export function first<T>(items: readonly T[]): T | undefined { return items[0] } export function getProperty<T, K extends keyof T>(object: T, key: K): T[K] { return object[key] } const user = { id: 7, name: 'Ирина' } const id = getProperty(user, 'id') // number const name = getProperty(user, 'name') // string export const preview = [first([id]), name]

В getProperty ограничение K extends keyof T не даёт передать несуществующий ключ, а тип результата T[K] зависит от выбранного ключа. Это самый частый пример на собеседовании: попробуйте объяснить, почему getProperty(user, 'email') не скомпилируется.

Ограничения и возвращаемый тип

Задача: сгруппировать массив по ключу. Важная деталь, которую ждёт интервьюер, - честный тип результата. Если вернуть Record<K, T[]>, TypeScript будет считать, что для любого ключа есть массив, хотя группы появляются только для встретившихся значений. Partial отражает это.

TypeScript// Группировка с честным типом: не каждый ключ обязательно встретится export function groupBy<T, K extends PropertyKey>( items: readonly T[], getKey: (item: T) => K, ): Partial<Record<K, T[]>> { const result: Partial<Record<K, T[]>> = {} for (const item of items) { const key = getKey(item) const group = result[key] ?? [] group.push(item) result[key] = group } return result }

PropertyKey - встроенный тип string | number | symbol: только такие значения могут быть ключами объекта. Ограничение не даёт по ошибке группировать по объекту, который превратился бы в строку «[object Object]».

Свои утилиты: mapped types и infer

Вторая классическая задача - написать Partial, Pick и ReturnType без стандартной библиотеки. Mapped type проходит по ключам типа, модификатор ? делает свойства необязательными. Условный тип с infer «вынимает» часть другого типа.

TSX// Свои версии утилит: mapped types и infer type MyPartial<T> = { [K in keyof T]?: T[K] } type MyPick<T, K extends keyof T> = { [P in K]: T[P] } type MyReturnType<F> = F extends (...args: never[]) => infer R ? R : never type ElementOf<T> = T extends readonly (infer E)[] ? E : never type User = { id: number; name: string; email?: string } type Draft = MyPartial<User> type Preview = MyPick<User, 'id' | 'name'> type Loaded = MyReturnType<() => Promise<User>> // Promise<User> type Tag = ElementOf<readonly ['react', 'node']> // 'react' | 'node' export type Examples = [Draft, Preview, Loaded, Tag]

Распределение по объединению

Условный тип над «голым» параметром распределяется по объединению: ElementOf<string[] | number[]> даст string | number. Иногда это неожиданно. Чтобы отключить распределение, оберните параметр в кортеж: [T] extends [U]. Этот нюанс любят спрашивать на позицию middle.

Частые ошибки

  • Дженерик ради дженерика: если T встречается один раз, он ничего не связывает - достаточно unknown.
  • Приведение as T внутри функции вместо проверки. Тип не проверяет данные во время выполнения - подробнее в разборе unknown на границе API.
  • Слишком широкое ограничение extends object, когда нужно конкретное свойство.
  • Забытый readonly у входного массива: функция не должна требовать изменяемый массив, если не меняет его.

Как отвечать на собеседовании

Начните с задачи, которую решает дженерик: сохранить связь типов. Затем покажите ограничение и вывод типов на примере. Если просят утилиту - сначала напишите mapped type, проговорите модификаторы, затем условный тип. Базовые темы TypeScript для первой работы собраны в статье TypeScript для junior frontend, а TypeScript - часть обоих треков менторства.

Источники

Справочник TypeScript: Generics, Keyof, Conditional Types, Mapped Types, Utility Types.

Вопросы по теме

Чем дженерик лучше any?
any отключает проверку, и связь между входом и выходом теряется. Дженерик сохраняет конкретный тип: функция, принявшая массив User, вернёт User, а не any.
Когда ставить ограничение extends?
Когда функция использует свойства параметра. Например, если нужно читать length, ограничьте T до { length: number }. Без ограничения TypeScript не позволит обращаться к свойству.
Почему ReturnType пишут с never[] в аргументах?
Параметры функции контравариантны. Тип (...args: never[]) => unknown совместим с любой функцией, поэтому условный тип срабатывает для функций с любыми аргументами.

Читайте также

Все статьи

Обсудим вашу подготовку?

Напишите, что уже умеете и с чем возникли трудности. Если есть проект, пришлите ссылку на него.

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

Что не получается, что уже пробовали. От 100 знаков, сейчас 0.
Где работали или учились, сколько лет, какой стек, сколько откликов и собеседований. От 100 знаков, сейчас 0.
PDF или DOCX, до 5 МБ. Файл видит только администратор.
GitHub, hh, LinkedIn, сайт. Можно несколько, через пробел или с новой строки.