Дженерики - тема, на которой видно, понимает ли кандидат систему типов или только расставляет аннотации. На собеседовании обычно просят объяснить идею, типизировать небольшую функцию и написать свою версию стандартной утилиты. Пройдём этот путь по шагам.
Идея: параметр типа
Функция 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.