Продвинутое собеседование по TypeScript: как впечатлить работодателя
TypeScript давно перестал быть просто «надстройкой» над JavaScript. На современных собеседованиях для Senior-позиций проверяют не только знание типов, но и умение работать с условными типами, маппингом, декораторами и системой вывода типов. В этой статье мы разберем 4 ключевые темы, которые помогут вам уверенно пройти техническое интервью по TypeScript на продвинутом уровне.
1. Условные типы (Conditional Types) и ключевое слово infer
Условные типы позволяют выбирать один из двух типов на основе условия. Они строятся по принципу тернарного оператора: T extends U ? X : Y. На собеседовании вас могут попросить написать тип, который извлекает возвращаемый тип функции.
// Пример: извлечение возвращаемого типа функции
type ReturnTypeExtract<T> = T extends (...args: any[]) => infer R ? R : never;
function greet(name: string): string {
return "Hello, " + name;
}
type GreetReturn = ReturnTypeExtract<typeof greet>; // string
Ключевое слово infer используется внутри условных типов для объявления новой переменной типа. Оно позволяет «вытащить» часть типа из сложной структуры. Часто спрашивают: «Что такое infer и где он применяется?». Ответ: для извлечения типов из промисов, функций, кортежей.
// Извлечение типа элемента из Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Result = UnwrapPromise<Promise<number>>; // number
type Result2 = UnwrapPromise<string>; // string
2. Маппированные типы (Mapped Types) и модификаторы readonly/optional
Маппированные типы позволяют создавать новые типы, перебирая свойства существующего. Это мощный инструмент для создания утилитарных типов вроде Partial<T> или Readonly<T>. На собеседовании могут попросить реализовать свой аналог Pick<T, K>.
// Собственная реализация Pick
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
interface User {
id: number;
name: string;
email: string;
}
type UserBasic = MyPick<User, 'id' | 'name'>;
// { id: number; name: string }
Обратите внимание на keyof T — это оператор, который возвращает объединение ключей объекта. В комбинации с маппингом он позволяет гибко трансформировать типы. Также часто спрашивают про модификаторы readonly и ? (optional). Их можно добавлять или удалять с помощью префиксов + и -:
// Удаление readonly из всех свойств
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
type ImmutableUser = Readonly<User>;
type MutableUser = Mutable<ImmutableUser>; // все поля снова изменяемы
3. Декораторы и их типизация
Декораторы — это экспериментальная фича TypeScript, которая активно используется в Angular и NestJS. На собеседовании вас могут спросить, как типизировать декоратор класса или метода. Важно понимать, что декоратор — это просто функция, принимающая целевой объект и возвращающая его (или новый).
// Декоратор класса с типизацией
function Logger<T extends { new (...args: any[]): {} }>(constructor: T) {
console.log(`Class ${constructor.name} created`);
return class extends constructor {
constructor(...args: any[]) {
super(...args);
console.log('Instance created');
}
};
}
@Logger
class MyService {
constructor(public name: string) {}
}
const service = new MyService('Test');
// В консоли: "Class MyService created", затем "Instance created"
В продвинутых вопросах могут попросить реализовать декоратор, который добавляет метаданные или валидацию. Обратите внимание на использование дженериков — <T extends { new (...args: any[]): {} }> гарантирует, что аргумент является конструктором.
4. Ковариация, контравариация и инвариантность
Это одна из самых сложных тем на собеседовании. В TypeScript типы могут быть ковариантными (сохраняют отношение наследования), контравариантными (обращают его) или инвариантными (не допускают подстановки). Проще всего это понять на примере функций.
// Ковариация для возвращаемого типа
type Animal = { name: string };
type Dog = Animal & { breed: string };
type ReturnFunc = () => Animal;
type ReturnFuncDog = () => Dog;
// ReturnFuncDog можно присвоить ReturnFunc, так как Dog — подтип Animal
let func1: ReturnFunc = () => ({ name: 'Any' });
let func2: ReturnFuncDog = () => ({ name: 'Rex', breed: 'Labrador' });
func1 = func2; // OK — ковариация
// Контравариация для аргументов
type ArgFunc = (arg: Animal) => void;
type ArgFuncDog = (arg: Dog) => void;
let func3: ArgFunc = (a: Animal) => {};
let func4: ArgFuncDog = (d: Dog) => {};
// func3 = func4; // Ошибка! Аргументы контравариантны
func4 = func3; // OK — можно присвоить функцию с более широким типом аргумента
На собеседовании могут спросить: «Почему в TypeScript параметры функций контравариантны?». Ответ: это обеспечивает безопасность типов — если функция ожидает Animal, а мы передадим Dog, то внутри функции могут быть вызваны методы, которых нет у Animal. TypeScript защищает от этого.
Заключение
Продвинутый уровень TypeScript — это не просто знание синтаксиса, а понимание того, как устроена система типов. Условные типы с infer, маппированные типы, декораторы и вариативность — это темы, которые отличают Junior-разработчика от Senior. Практикуйте написание собственных утилитарных типов, разбирайте реальные примеры из библиотек (например, ReturnType из TypeScript) и не бойтесь задавать вопросы на собеседовании. Удачи!