Введение: почему TypeScript — must have для современного разработчика
TypeScript прочно занял нишу стандарта де-факто в enterprise-разработке. По данным опросов Stack Overflow, более 70% frontend-разработчиков используют или планируют использовать TypeScript. На собеседованиях без вопросов по этой технологии не обходится — даже если вакансия формально указана как "JavaScript Developer".
В этой статье мы разберём ключевые темы, которые вам обязательно зададут на техническом интервью: от базовых типов до сложных дженериков и условных типов. Материал построен по принципу "от простого к сложному", чтобы вы могли системно подготовиться.
1. База: типы, интерфейсы и их отличия
1.1. Примитивные и специальные типы
Начнём с основ. Интервьюер почти наверняка спросит про any, unknown, never и void.
any— отключает проверку типов. Используется только как крайняя мера при миграции.unknown— безопасная альтернатива any. Требует проверки типа перед использованием.never— тип, который никогда не происходит (функции, выбрасывающие исключения, или бесконечные циклы).void— отсутствие возвращаемого значения у функции.
function throwError(msg: string): never {
throw new Error(msg);
}
function log(msg: string): void {
console.log(msg);
}
let data: unknown = JSON.parse('{"name":"John"}');
// data.name // Ошибка! Нужна проверка
if (typeof data === 'object' && data !== null) {
console.log((data as Record<string, unknown>).name);
}
1.2. Interface vs Type: что выбрать?
Это классический вопрос. Оба подхода позволяют описывать структуру объекта, но есть нюансы:
- Interface — расширяем (
extends), может быть объединён (declaration merging). - Type — может описывать union-типы, intersection, примитивы. Не подлежит declaration merging.
// Interface - расширение
interface User {
name: string;
}
interface User {
age: number; // declaration merging
}
const user: User = { name: 'Alice', age: 30 };
// Type - union
type Status = 'active' | 'inactive';
type UserOrStatus = User | Status;
Совет: Для объектов и классов используйте interface, для сложных композиций — type.
2. Дженерики и утилитарные типы
2.1. Дженерики: обобщённое программирование
Без дженериков не обходится ни одно серьёзное интервью. Умение написать тип, который адаптируется под переданный аргумент, — показатель зрелости разработчика.
// Простой дженерик
function identity<T>(arg: T): T {
return arg;
}
// Ограничение дженерика
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
// Дженерик для массива
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
Часто спрашивают про infer — ключевое слово для вывода типов внутри условных конструкций:
// Извлекаем тип элемента из Promise
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type Result = Unwrap<Promise<number>>; // number
2.2. Утилитарные типы (Utility Types)
Знание встроенных утилит — обязательный минимум. Интервьюер может попросить реализовать аналог вручную.
Partial<T>— все поля становятся опциональными.Required<T>— все поля обязательные.Pick<T, K>— выбирает только указанные ключи.Omit<T, K>— исключает указанные ключи.Record<K, V>— создаёт тип с ключами K и значениями V.
interface User {
id: number;
name: string;
email: string;
}
// Partial - все поля необязательны
type PartialUser = Partial<User>;
// Pick - только id и name
type UserPreview = Pick<User, 'id' | 'name'>;
// Record
type Role = 'admin' | 'user';
type RolePermissions = Record<Role, string[]>;
Вопрос с подвохом: "Как сделать глубокий Partial (DeepPartial)?" — это проверка на понимание рекурсивных типов.
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
3. Advanced-темы: условные типы и mapped types
3.1. Условные типы (Conditional Types)
Позволяют создавать типы, которые зависят от условий. Это мощный инструмент для библиотек и фреймворков.
// Базовый условный тип
type IsString<T> = T extends string ? 'yes' : 'no';
type Test1 = IsString<'hello'>; // 'yes'
type Test2 = IsString<number>; // 'no'
// Распределительные условные типы (Distributive Conditional Types)
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]
На собеседовании могут попросить объяснить, почему string | number превращается в string[] | number[], а не в (string | number)[]. Ответ: TypeScript автоматически дистрибутивно применяет условный тип к каждому члену union.
3.2. Mapped Types (отображаемые типы)
Позволяют трансформировать существующие типы, перебирая их ключи. Классический пример — сделать все поля readonly:
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};