Вопросы и ответы по TypeScript
Подборка популярных вопросов на собеседованиях по TypeScript с подробными ответами и примерами кода.
1. Основные типы
Вопрос: В чём разница между interface и type?
typescript
// Interface — описывает структуру объекта
interface User {
name: string;
age: number;
}
// Interface можно расширять (declaration merging)
interface User {
email: string;
}
// Теперь User имеет name, age и email
// Type alias — может описывать любой тип
type ID = string | number;
type Point = {
x: number;
y: number;
};
// Type нельзя переопределить (declaration merging не работает)
// type ID = boolean; // Ошибка: Duplicate identifier 'ID'Пояснение:
interfaceподдерживает declaration merging (слияние деклараций)typeможет описывать union, intersection, примитивы, кортежиinterfaceможно расширять черезextends,type— через&- Для объектов и классов предпочтительнее
interface, для остального —type
Вопрос: Что такое unknown и чем он отличается от any?
typescript
// any — отключает проверку типов
let valueAny: any = 42;
valueAny.foo.bar; // Нет ошибки компиляции (но упадёт в runtime)
valueAny.toUpperCase(); // Нет ошибки
// unknown — безопасная альтернатива any
let valueUnknown: unknown = 42;
// valueUnknown.foo; // Ошибка: Object is of type 'unknown'
// valueUnknown.toUpperCase(); // Ошибка
// Нужно сузить тип перед использованием
if (typeof valueUnknown === 'string') {
valueUnknown.toUpperCase(); // OK
}Пояснение:
anyполностью отключает типизацию — можно делать что угодноunknownтребует проверки типа перед использованиемunknown— type-safe аналогany, рекомендуется использовать вместоany
2. Generics (обобщения)
Вопрос: Реализуйте типизированную функцию getProperty
typescript
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: 'Иван', age: 30, isAdmin: true };
const name = getProperty(person, 'name'); // тип: string
const age = getProperty(person, 'age'); // тип: number
// getProperty(person, 'email'); // Ошибка: 'email' не существуетПояснение:
K extends keyof Tограничивает ключ только существующими свойствами объектаT[K]— indexed access type, возвращает тип значения по ключу- TypeScript автоматически выводит типы
TиKиз аргументов
Вопрос: Что такое Generic Constraints и зачем они нужны?
typescript
// Без ограничений — не знаем, есть ли свойство length
// function logLength<T>(arg: T): void {
// console.log(arg.length); // Ошибка: Property 'length' does not exist
// }
// С ограничением
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): void {
console.log(arg.length); // OK
}
logLength('hello'); // 5
logLength([1, 2, 3]); // 3
logLength({ length: 10 }); // 10
// logLength(42); // Ошибка: number не имеет свойства lengthПояснение:
T extends HasLengthгарантирует, что тип имеет свойствоlength- Это позволяет использовать generic с конкретными требованиями к типу
- Constraints делают generic-функции более предсказуемыми
3. Utility Types
Вопрос: Реализуйте свой Partial<T> и Required<T>
typescript
// Встроенный Partial делает все свойства опциональными
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
// Встроенный Required делает все свойства обязательными
type MyRequired<T> = {
[P in keyof T]-?: T[P];
};
// Пример
interface Config {
host: string;
port: number;
debug?: boolean;
}
type PartialConfig = MyPartial<Config>;
// { host?: string; port?: number; debug?: boolean; }
type RequiredConfig = MyRequired<Config>;
// { host: string; port: number; debug: boolean; }
function createConfig(overrides: MyPartial<Config>): Config {
return {
host: 'localhost',
port: 3000,
debug: false,
...overrides,
};
}Вопрос: Реализуйте свой Pick<T, K> и Omit<T, K>
typescript
// Pick — выбирает указанные свойства
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Omit — исключает указанные свойства
type MyOmit<T, K extends keyof T> = {
[P in Exclude<keyof T, K>]: T[P];
};
interface User {
id: number;
name: string;
email: string;
password: string;
}
type PublicUser = MyOmit<User, 'password'>;
// { id: number; name: string; email: string; }
type UserCredentials = MyPick<User, 'email' | 'password'>;
// { email: string; password: string; }4. Type Guards (сужение типов)
Вопрос: Какие способы сужения типов существуют в TypeScript?
typescript
// 1. typeof
function processValue(value: string | number) {
if (typeof value === 'string') {
console.log(value.toUpperCase()); // string
} else {
console.log(value.toFixed(2)); // number
}
}
// 2. instanceof
class Cat {
meow() { console.log('Мяу!'); }
}
class Dog {
bark() { console.log('Гав!'); }
}
function makeSound(animal: Cat | Dog) {
if (animal instanceof Cat) {
animal.meow();
} else {
animal.bark();
}
}
// 3. in operator
interface Fish {
swim: () => void;
}
interface Bird {
fly: () => void;
}
function move(animal: Fish | Bird) {
if ('swim' in animal) {
animal.swim();
} else {
animal.fly();
}
}
// 4. Пользовательский type guard (type predicate)
function isFish(animal: Fish | Bird): animal is Fish {
return (animal as Fish).swim !== undefined;
}
const pet: Fish | Bird = { swim: () => console.log('Плыву') };
if (isFish(pet)) {
pet.swim(); // TypeScript знает, что это Fish
}Вопрос: Что такое Discriminated Unions?
typescript
// Дискриминированные объединения используют общее поле-дискриминант
interface Circle {
kind: 'circle';
radius: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
interface Triangle {
kind: 'triangle';
base: number;
height: number;
}
type Shape = Circle | Rectangle | Triangle;
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
default:
// Exhaustive check — ошибка, если не все варианты обработаны
const _exhaustive: never = shape;
return _exhaustive;
}
}Пояснение:
- Поле
kind— дискриминант, общее для всех типов в union - TypeScript автоматически сужает тип в каждой ветке
switch - Паттерн
neverвdefaultгарантирует обработку всех вариантов
5. Условные типы и infer
Вопрос: Как работают условные типы?
typescript
// Условный тип — тернарный оператор для типов
type IsString<T> = T extends string ? 'да' : 'нет';
type A = IsString<string>; // 'да'
type B = IsString<number>; // 'нет'
type C = IsString<'hello'>; // 'да'
// Практический пример: извлечение типа элемента массива
type ElementType<T> = T extends (infer U)[] ? U : T;
type D = ElementType<string[]>; // string
type E = ElementType<number[]>; // number
type F = ElementType<boolean>; // boolean (не массив — возвращает как есть)
// Извлечение типа возвращаемого значения функции
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function greet(name: string): string {
return `Привет, ${name}!`;
}
type GreetReturn = MyReturnType<typeof greet>; // stringПояснение:
T extends U ? X : Y— еслиTрасширяетU, то типX, иначеYinferпозволяет извлечь тип из структуры- Условные типы распределяются по union-типам (distributive conditional types)
Вопрос: Что такое Template Literal Types?
typescript
// Шаблонные литеральные типы
type Color = 'red' | 'green' | 'blue';
type Size = 'small' | 'medium' | 'large';
type Style = `${Size}-${Color}`;
// 'small-red' | 'small-green' | 'small-blue'
// | 'medium-red' | 'medium-green' | 'medium-blue'
// | 'large-red' | 'large-green' | 'large-blue'
// Практический пример: типизация событий
type EventName = 'click' | 'focus' | 'blur';
type EventHandler = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur'
// Извлечение частей строки
type ExtractId<T extends string> =
T extends `user_${infer Id}` ? Id : never;
type UserId = ExtractId<'user_123'>; // '123'
type Invalid = ExtractId<'admin_456'>; // never6. Mapped Types
Вопрос: Реализуйте тип, делающий все свойства readonly рекурсивно
typescript
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? T[P] extends Function
? T[P]
: DeepReadonly<T[P]>
: T[P];
};
interface NestedConfig {
database: {
host: string;
port: number;
credentials: {
user: string;
password: string;
};
};
features: string[];
}
type ReadonlyConfig = DeepReadonly<NestedConfig>;
const config: ReadonlyConfig = {
database: {
host: 'localhost',
port: 5432,
credentials: {
user: 'admin',
password: 'secret',
},
},
features: ['auth', 'api'],
};
// config.database.host = 'remote'; // Ошибка: readonly
// config.database.credentials.user = 'root'; // Ошибка: readonlyВопрос: Как создать тип с динамическими ключами?
typescript
// Record — создаёт тип с указанными ключами и значениями
type Roles = 'admin' | 'user' | 'guest';
type RolePermissions = Record<Roles, string[]>;
const permissions: RolePermissions = {
admin: ['read', 'write', 'delete'],
user: ['read', 'write'],
guest: ['read'],
};
// Mapped type с преобразованием ключей (Key Remapping)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }7. Классы и паттерны
Вопрос: Реализуйте паттерн Singleton с TypeScript
typescript
class Database {
private static instance: Database;
private connectionString: string;
private constructor(connectionString: string) {
this.connectionString = connectionString;
}
static getInstance(connectionString?: string): Database {
if (!Database.instance) {
Database.instance = new Database(connectionString || 'default');
}
return Database.instance;
}
query(sql: string): void {
console.log(`[${this.connectionString}] Executing: ${sql}`);
}
}
// const db = new Database('...'); // Ошибка: конструктор приватный
const db1 = Database.getInstance('postgres://localhost/mydb');
const db2 = Database.getInstance();
console.log(db1 === db2); // trueВопрос: Чем отличаются abstract class и interface?
typescript
// Abstract class — может содержать реализацию
abstract class Animal {
constructor(protected name: string) {}
// Реализованный метод
greet(): string {
return `Я ${this.name}`;
}
// Абстрактный метод — должен быть реализован в подклассе
abstract makeSound(): string;
}
class Cat extends Animal {
makeSound(): string {
return 'Мяу!';
}
}
// Interface — только контракт, без реализации
interface Printable {
print(): void;
}
// Класс может реализовать несколько интерфейсов
// но наследоваться только от одного абстрактного класса
class Report extends Animal implements Printable {
makeSound(): string {
return 'Шелест бумаги';
}
print(): void {
console.log(`Отчёт: ${this.greet()}`);
}
}Пояснение:
abstract classможет содержать реализацию методов и свойства с модификаторами доступаinterfaceописывает только контракт (форму)- Класс наследует только один
abstract class, но реализует множествоinterface abstract classкомпилируется в JavaScript-код,interface— стирается при компиляции
8. Продвинутые паттерны типов
Вопрос: Как типизировать функцию с перегрузками?
typescript
// Перегрузки функций
function parse(input: string): number;
function parse(input: number): string;
function parse(input: string | number): string | number {
if (typeof input === 'string') {
return parseInt(input, 10);
}
return input.toString();
}
const num = parse('42'); // тип: number
const str = parse(42); // тип: string
// parse(true); // Ошибка: нет подходящей перегрузкиВопрос: Что такое satisfies и когда его использовать?
typescript
// satisfies проверяет тип без расширения
type Colors = Record<string, string | string[]>;
// С обычной аннотацией типа — теряем конкретный тип
const colorsTyped: Colors = {
primary: 'red',
secondary: ['green', 'blue'],
};
// colorsTyped.primary.toUpperCase(); // Ошибка: может быть string[]
// С satisfies — сохраняем точный тип
const colorsSatisfies = {
primary: 'red',
secondary: ['green', 'blue'],
} satisfies Colors;
colorsSatisfies.primary.toUpperCase(); // OK — TypeScript знает, что это string
colorsSatisfies.secondary.join(', '); // OK — TypeScript знает, что это string[]Вопрос: Как работает const assertion?
typescript
// Без as const — типы расширяются
const config1 = {
endpoint: '/api',
method: 'GET',
};
// тип: { endpoint: string; method: string; }
// С as const — литеральные типы + readonly
const config2 = {
endpoint: '/api',
method: 'GET',
} as const;
// тип: { readonly endpoint: '/api'; readonly method: 'GET'; }
// Полезно для создания enum-подобных объектов
const Direction = {
Up: 'UP',
Down: 'DOWN',
Left: 'LEFT',
Right: 'RIGHT',
} as const;
type DirectionType = typeof Direction[keyof typeof Direction];
// 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'
function move(direction: DirectionType) {
console.log(`Движение: ${direction}`);
}
move(Direction.Up); // OK
// move('DIAGONAL'); // ОшибкаЗаключение
Советы для подготовки к собеседованию по TypeScript:
- Разберитесь в системе типов:
unknownvsany,never, union/intersection - Практикуйтесь в написании utility types с mapped types и conditional types
- Понимайте разницу между
interfaceиtype,abstract classиinterface - Изучите
infer, template literal types и key remapping - Умейте объяснить, как TypeScript выводит типы (type inference)
- Знайте, как работают type guards и discriminated unions
Удачи на собеседовании!