Перейти к основному содержимому

200 вопросов по TypeScript

200 вопросов по TypeScript

Основы TypeScript

Вопрос

Что такое TypeScript?

Ответ

TypeScript — это язык программирования, расширяющий JavaScript статической типизацией, классами, интерфейсами и другими возможностями, которые компилируются в чистый JavaScript.


Вопрос

Какие основные преимущества TypeScript перед JavaScript?

Ответ

TypeScript обеспечивает статическую проверку типов во время разработки, улучшает автодополнение в редакторах, поддерживает современные конструкции языка даже при транспиляции в старые версии JavaScript и снижает количество ошибок времени выполнения.


Вопрос

Как установить TypeScript?

Ответ

TypeScript устанавливается через npm командой npm install -g typescript. После установки доступна команда tsc для компиляции .ts-файлов в .js.


Вопрос

Как скомпилировать файл TypeScript в JavaScript?

Ответ

Файл компилируется с помощью команды tsc имя_файла.ts. Результат — файл с тем же именем и расширением .js.


Вопрос

Что такое файл tsconfig.json?

Ответ

Файл tsconfig.json содержит конфигурацию проекта TypeScript: параметры компиляции, пути к исходным файлам, целевую версию ECMAScript и другие настройки.


Вопрос

Как указать целевую версию JavaScript при компиляции?

Ответ

Целевая версия задаётся в tsconfig.json свойством "target". Например: "target": "ES2020".


Вопрос

Можно ли использовать TypeScript без компиляции?

Ответ

Нет, TypeScript требует компиляции в JavaScript, потому что браузеры и среды выполнения не понимают синтаксис TypeScript напрямую.


Вопрос

Поддерживает ли TypeScript все возможности JavaScript?

Ответ

Да, любой корректный JavaScript-код является корректным TypeScript-кодом. TypeScript является надмножеством JavaScript.


Вопрос

Что такое типизация в TypeScript?

Ответ

Типизация — это механизм явного или неявного указания типа данных переменной, параметра функции или возвращаемого значения, который используется для статической проверки кода.


Вопрос

Какие способы объявления типов существуют в TypeScript?

Ответ

Типы можно указывать явно после имени переменной через двоеточие или полагаться на вывод типов, когда TypeScript автоматически определяет тип на основе присваиваемого значения.

let age: number = 30;      // явное указание
let name = "Alice"; // вывод типа string

Вопрос

Какие примитивные типы есть в TypeScript?

Ответ

Примитивные типы: number, string, boolean, bigint, symbol, null, undefined.


Вопрос

Чем отличаются типы null и undefined?

Ответ

null обозначает намеренное отсутствие значения. undefined означает, что значение не было присвоено.


Вопрос

Что такое литеральные типы?

Ответ

Литеральные типы ограничивают значение конкретной строкой, числом или булевым значением. Например: type Direction = "left" | "right".


Вопрос

Как объявить переменную, которая может принимать несколько типов?

Ответ

Используется объединение типов через вертикальную черту:

let id: string | number;
id = "123";
id = 456;

Вопрос

Что такое тип any?

Ответ

Тип any отключает проверку типов для переменной. Он позволяет присваивать любые значения и вызывать любые методы без ошибок компиляции.


Вопрос

Когда уместно использовать any?

Ответ

any уместен при миграции JavaScript-кода на TypeScript или при работе с динамическими данными, тип которых невозможно определить заранее. Его использование рекомендуется минимизировать.


Вопрос

Что такое тип unknown?

Ответ

Тип unknown представляет значение неизвестного типа. В отличие от any, с ним нельзя выполнять операции без предварительной проверки типа.


Вопрос

Как безопасно работать с типом unknown?

Ответ

Перед использованием требуется проверка типа через typeof, instanceof или пользовательские функции-предикаты.

function process(value: unknown) {
if (typeof value === "string") {
console.log(value.toUpperCase());
}
}

Вопрос

Что такое тип never?

Ответ

Тип never представляет значения, которые никогда не возникают. Он используется для функций, которые всегда выбрасывают исключение или зацикливаются бесконечно.


Вопрос

Приведите пример функции с возвращаемым типом never.

Ответ

function throwError(message: string): never {
throw new Error(message);
}

Вопрос

Что такое вывод типов?

Ответ

Вывод типов — это механизм, при котором TypeScript автоматически определяет тип переменной на основе её начального значения.


Функции и параметры

Вопрос

Как объявить функцию с типизированными параметрами и возвращаемым значением?

Ответ

Указывается тип каждого параметра после его имени и тип возвращаемого значения после скобок:

function add(a: number, b: number): number {
return a + b;
}

Вопрос

Что такое необязательный параметр функции?

Ответ

Необязательный параметр помечается знаком вопроса ? и может быть опущен при вызове функции:

function greet(name: string, title?: string): string {
return title ? `${title} ${name}` : name;
}

Вопрос

Можно ли задать значение по умолчанию для параметра?

Ответ

Да, значение по умолчанию задаётся через присваивание. Такой параметр автоматически становится необязательным:

function log(message: string, level: string = "info"): void {
console.log(`[${level}] ${message}`);
}

Вопрос

Что такое rest-параметры в TypeScript?

Ответ

Rest-параметры позволяют принимать переменное количество аргументов как массив. Объявляются с помощью троеточия ...:

function sum(...numbers: number[]): number {
return numbers.reduce((acc, n) => acc + n, 0);
}

Вопрос

Как указать тип функции как переменной?

Ответ

Тип функции записывается в виде сигнатуры: (параметры) => возвращаемый_тип:

let calculator: (x: number, y: number) => number;
calculator = (a, b) => a * b;

Вопрос

Что такое перегрузка функций?

Ответ

Перегрузка функций — это объявление нескольких сигнатур одной функции с разными типами параметров или возвращаемых значений. Реализация одна, но она должна покрывать все объявленные варианты:

function process(input: string): string;
function process(input: number): number;
function process(input: string | number): string | number {
return input;
}

Вопрос

Поддерживает ли TypeScript стрелочные функции?

Ответ

Да, стрелочные функции полностью поддерживаются и наследуют поведение из JavaScript, включая лексический this.


Вопрос

Как типизировать функцию обратного вызова (callback)?

Ответ

Callback типизируется как параметр с функциональной сигнатурой:

function fetchData(callback: (data: string) => void): void {
callback("result");
}

Объекты и интерфейсы

Вопрос

Что такое интерфейс в TypeScript?

Ответ

Интерфейс — это контракт, описывающий форму объекта: имена свойств, их типы и методы. Он используется только во время компиляции.

interface User {
id: number;
name: string;
}

Вопрос

Можно ли добавлять методы в интерфейс?

Ответ

Да, интерфейс может содержать сигнатуры методов:

interface Logger {
log(message: string): void;
error(err: string): void;
}

Вопрос

Что такое необязательное свойство в интерфейсе?

Ответ

Свойство, помеченное знаком вопроса ?, может отсутствовать в объекте:

interface Config {
theme: string;
timeout?: number;
}

Вопрос

Можно ли расширять интерфейсы?

Ответ

Да, один интерфейс может наследовать другой с помощью ключевого слова extends:

interface Animal {
name: string;
}

interface Dog extends Animal {
breed: string;
}

Вопрос

Что происходит при реализации интерфейса классом?

Ответ

Класс обязан реализовать все свойства и методы, объявленные в интерфейсе:

interface Drawable {
draw(): void;
}

class Circle implements Drawable {
draw(): void {
console.log("Drawing a circle");
}
}

Вопрос

Можно ли использовать интерфейс для описания функции?

Ответ

Да, интерфейс может содержать сигнатуру вызова:

interface Adder {
(a: number, b: number): number;
}

const sum: Adder = (x, y) => x + y;

Вопрос

Что такое индексные сигнатуры?

Ответ

Индексные сигнатуры позволяют описать объект, у которого ключи и значения имеют определённые типы:

interface StringMap {
[key: string]: string;
}

const map: StringMap = { a: "alpha", b: "beta" };

Вопрос

Можно ли комбинировать несколько интерфейсов при объявлении типа объекта?

Ответ

Да, с помощью пересечения типов (&) можно объединить несколько интерфейсов:

interface A { a: string; }
interface B { b: number; }

const obj: A & B = { a: "test", b: 42 };

Вопрос

Чем отличается interface от type?

Ответ

interface создаёт именованный контракт и поддерживает слияние объявлений. type создаёт псевдоним для любого типа, включая примитивы, объединения и кортежи. Интерфейсы предпочтительны для описания объектов и классов.


Вопрос

Можно ли переопределять интерфейс?

Ответ

Да, TypeScript позволяет объявлять интерфейс с тем же именем несколько раз. Все объявления объединяются в один.

interface User {
name: string;
}

interface User {
age: number;
}

// Эквивалентно: interface User { name: string; age: number; }

Вопрос

Как описать объект с динамическими ключами?

Ответ

Используется индексная сигнатура с типом ключа string или number:

interface Data {
[key: string]: unknown;
}

Вопрос

Что такое readonly-свойство в интерфейсе?

Ответ

Свойство, помеченное как readonly, не может быть изменено после инициализации:

interface Point {
readonly x: number;
readonly y: number;
}

Вопрос

Можно ли сделать весь объект доступным только для чтения?

Ответ

Да, с помощью утилитарного типа Readonly<T>:

interface Config { url: string; retries: number; }
const config: Readonly<Config> = { url: "api", retries: 3 };
// config.retries = 5; // Ошибка

Вопрос

Как проверить, соответствует ли объект интерфейсу?

Ответ

TypeScript выполняет структурную типизацию: если объект содержит все требуемые свойства с совместимыми типами, он считается совместимым с интерфейсом. Явная проверка во время выполнения невозможна, так как интерфейсы удаляются при компиляции.


Вопрос

Что такое excess property check?

Ответ

Это проверка на наличие лишних свойств при присваивании литерала объекта переменной с типом интерфейса. Она срабатывает только для объектных литералов:

interface User { name: string; }
const u: User = { name: "Alice", age: 30 }; // Ошибка: "age" не существует в User

Вопрос

Как обойти проверку лишних свойств?

Ответ

Можно присвоить объект переменной промежуточного типа или использовать утверждение типа:

const raw = { name: "Alice", age: 30 };
const u: User = raw; // OK

// Или:
const u = { name: "Alice", age: 30 } as User;

Классы и наследование

Вопрос

Как объявить класс в TypeScript?

Ответ

Класс объявляется с помощью ключевого слова class, за которым следует имя класса и тело в фигурных скобках:

class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}

Вопрос

Что такое конструктор в классе?

Ответ

Конструктор — это специальный метод с именем constructor, который вызывается при создании экземпляра класса. Он используется для инициализации свойств объекта.


Вопрос

Можно ли использовать параметры конструктора для автоматического создания и инициализации свойств?

Ответ

Да, если перед параметром указать модификатор доступа (public, private, protected или readonly), TypeScript автоматически создаст и инициализирует соответствующее свойство:

class Point {
constructor(public x: number, public y: number) {}
}
// Эквивалентно ручному объявлению x и y + присваиванию в теле конструктора

Вопрос

Какие модификаторы доступа существуют в TypeScript?

Ответ

Существуют четыре модификатора доступа:

  • public — свойство или метод доступен из любого места (по умолчанию);
  • private — доступен только внутри класса;
  • protected — доступен внутри класса и его потомков;
  • readonly — свойство доступно только для чтения после инициализации.

Вопрос

Что делает модификатор private?

Ответ

Модификатор private ограничивает доступ к свойству или методу только внутри класса, в котором он объявлен. Попытка доступа извне вызывает ошибку компиляции.

class Secret {
private code = "123";
}
const s = new Secret();
// s.code; // Ошибка: 'code' is private

Вопрос

Что делает модификатор protected?

Ответ

Модификатор protected позволяет обращаться к свойству или методу из самого класса и всех его подклассов, но не извне.

class Animal {
protected name: string;
constructor(name: string) { this.name = name; }
}

class Dog extends Animal {
bark() { console.log(`${this.name} barks`); } // OK
}

Вопрос

Что такое наследование в TypeScript?

Ответ

Наследование — это механизм, позволяющий одному классу (потомку) расширять функциональность другого класса (родителя) с помощью ключевого слова extends.


Вопрос

Как вызвать конструктор родительского класса из дочернего?

Ответ

Конструктор родительского класса вызывается с помощью ключевого слова super():

class Animal {
constructor(public name: string) {}
}

class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name);
}
}

Вопрос

Можно ли переопределять методы родительского класса?

Ответ

Да, методы можно переопределять в дочернем классе. При этом сигнатура метода должна быть совместимой с родительской.

class Animal {
makeSound() { console.log("Some sound"); }
}

class Dog extends Animal {
makeSound() { console.log("Woof!"); } // Переопределение
}

Вопрос

Что такое абстрактный класс?

Ответ

Абстрактный класс — это класс, помеченный ключевым словом abstract, который не может быть создан напрямую и предназначен для наследования. Он может содержать как реализованные, так и абстрактные методы.

abstract class Shape {
abstract getArea(): number;
toString() { return "Shape"; }
}

Вопрос

Что такое абстрактный метод?

Ответ

Абстрактный метод объявляется без реализации и помечается ключевым словом abstract. Любой неабстрактный подкласс обязан реализовать все абстрактные методы.

abstract class Component {
abstract render(): void;
}

Вопрос

Можно ли создавать экземпляр абстрактного класса?

Ответ

Нет, попытка создать экземпляр абстрактного класса приведёт к ошибке компиляции. Абстрактные классы предназначены только для наследования.


Вопрос

Что такое геттеры и сеттеры?

Ответ

Геттеры и сеттеры — это специальные методы, которые позволяют контролировать доступ к свойству. Объявляются с помощью ключевых слов get и set.

class Temperature {
private _celsius: number = 0;
get celsius() { return this._celsius; }
set celsius(value: number) { this._celsius = value; }
}

Вопрос

Как сделать свойство статическим?

Ответ

Свойство или метод объявляется статическим с помощью ключевого слова static. Такие члены принадлежат самому классу, а не его экземплярам.

class MathUtils {
static PI = 3.14159;
static double(x: number): number { return x * 2; }
}
// Использование: MathUtils.PI, MathUtils.double(5)

Вопрос

Что такое параметризованный (дженерик) класс?

Ответ

Параметризованный класс — это класс, который принимает один или несколько типовых параметров, позволяя использовать обобщённую логику с разными типами:

class Box<T> {
constructor(public value: T) {}
}

const numberBox = new Box<number>(42);
const stringBox = new Box<string>("hello");

Вопрос

Можно ли применять интерфейсы к классам?

Ответ

Да, класс может реализовывать один или несколько интерфейсов с помощью ключевого слова implements:

interface Flyable {
fly(): void;
}

class Bird implements Flyable {
fly() { console.log("Flying"); }
}

Вопрос

Что происходит, если класс реализует интерфейс, но не предоставляет все его члены?

Ответ

TypeScript выдаст ошибку компиляции, требуя реализацию всех обязательных свойств и методов, объявленных в интерфейсе.


Вопрос

Можно ли комбинировать наследование и реализацию интерфейсов?

Ответ

Да, класс может одновременно наследовать от другого класса и реализовывать один или несколько интерфейсов:

class Vehicle { move() {} }
interface Drivable { drive(): void; }

class Car extends Vehicle implements Drivable {
drive() { console.log("Driving"); }
}

Вопрос

Что такое индексные свойства в классах?

Ответ

Индексные свойства позволяют описать поведение доступа к элементам объекта по индексу. В классах они редко используются, чаще — в интерфейсах или объектных типах.

class Dictionary {
[key: string]: any;
}

Вопрос

Как запретить изменение свойства после инициализации?

Ответ

Свойство помечается как readonly. Оно может быть установлено только в конструкторе или при объявлении.

class ImmutablePoint {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}

Вопрос

Что такое свойства уровня класса (class fields)?

Ответ

Свойства уровня класса — это поля, объявленные прямо в теле класса, без необходимости их явной инициализации в конструкторе (если указан тип или значение по умолчанию):

class Counter {
count: number = 0;
increment() { this.count++; }
}

Вопрос

Поддерживает ли TypeScript приватные поля стандарта ECMAScript?

Ответ

Да, TypeScript поддерживает приватные поля с использованием символа #, как в современном JavaScript:

class Safe {
#secret = "top";
reveal() { return this.#secret; }
}
// Доступ к #secret извне невозможен даже во время выполнения

Вопрос

В чём разница между private и #private?

Ответ

private — это проверка времени компиляции, которая исчезает в сгенерированном JavaScript. #private — это настоящая приватность на уровне движка JavaScript, недоступная даже через рефлексию.


Вопрос

Как обеспечить, что класс имеет только один экземпляр (паттерн Singleton)?

Ответ

Реализуется через приватный конструктор и статический метод или свойство, возвращающее единственный экземпляр:

class Singleton {
private static instance: Singleton;
private constructor() {}
static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}

Вопрос

Можно ли наследовать от нескольких классов?

Ответ

Нет, TypeScript, как и JavaScript, поддерживает только одиночное наследование. Для множественного повторного использования логики применяются миксины или композиция.


Дженерики (Generics)

Вопрос

Что такое дженерики в TypeScript?

Ответ

Дженерики — это механизм параметризации типов, позволяющий писать функции, классы и интерфейсы, которые работают с любыми типами данных, сохраняя при этом статическую типизацию.

function identity<T>(arg: T): T {
return arg;
}

Вопрос

Как объявить дженерик-функцию?

Ответ

Дженерик-функция объявляется с угловыми скобками после имени, содержащими параметр типа:

function log<T>(value: T): void {
console.log(value);
}
log<string>("hello");
log<number>(42);

Вопрос

Можно ли использовать несколько параметров типа в дженерике?

Ответ

Да, можно указать несколько параметров через запятую:

function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}

Вопрос

Как задать ограничение (constraint) для параметра типа?

Ответ

Ограничение задаётся с помощью ключевого слова extends, чтобы гарантировать наличие определённых свойств или методов:

interface Lengthwise {
length: number;
}

function logLength<T extends Lengthwise>(arg: T): void {
console.log(arg.length);
}

Вопрос

Можно ли использовать дженерики в интерфейсах?

Ответ

Да, интерфейсы могут быть параметризованы:

interface Box<T> {
value: T;
}

const stringBox: Box<string> = { value: "text" };

Вопрос

Как использовать дженерики в классах?

Ответ

Класс объявляется с параметром типа после имени, и этот тип может использоваться в свойствах и методах:

class Container<T> {
constructor(public item: T) {}
}

const numContainer = new Container<number>(100);

Вопрос

Что такое вывод типа аргумента в дженериках?

Ответ

TypeScript может автоматически вывести тип параметра на основе переданного значения, без явного указания:

function createArray<T>(item: T, count: number): T[] {
return Array(count).fill(item);
}

const arr = createArray("hi", 3); // Тип T выведен как string

Вопрос

Можно ли задать значение по умолчанию для параметра типа?

Ответ

Да, начиная с TypeScript 2.3, можно указать тип по умолчанию:

interface Response<T = string> {
data: T;
}

const resp: Response = { data: "ok" }; // T = string

Вопрос

Как использовать дженерики с функциями обратного вызова?

Ответ

Параметр типа может передаваться в callback, обеспечивая типобезопасность:

function map<T, U>(array: T[], fn: (item: T) => U): U[] {
return array.map(fn);
}

const numbers = [1, 2, 3];
const strings = map(numbers, n => n.toString()); // string[]

Вопрос

Что такое условные типы (conditional types)?

Ответ

Условные типы позволяют выбирать один из двух типов на основе условия, аналогично тернарному оператору:

type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // true
type B = IsString<42>; // false

Вопрос

Как объявить условный тип с дженериком?

Ответ

Условный тип объявляется внутри выражения типа с использованием extends:

type TypeName<T> = T extends string
? "string"
: T extends number
? "number"
: "other";

type T1 = TypeName<"a">; // "string"
type T2 = TypeName<1>; // "number"

Вопрос

Что такое распределительные условные типы?

Ответ

Если параметр типа в условном типе является объединением, TypeScript автоматически применяет условие к каждому элементу объединения:

type ToArray<T> = T extends any ? T[] : never;
type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]

Вопрос

Как запретить распределение в условных типах?

Ответ

Оборачивание параметра типа в кортеж предотвращает распределение:

type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type T = ToArrayNonDist<string | number>; // (string | number)[]

Вопрос

Что такое mapped types?

Ответ

Mapped types создают новый тип на основе другого, применяя преобразование к каждому свойству:

type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

interface Person { name: string; age: number; }
type ReadonlyPerson = Readonly<Person>;

Вопрос

Как использовать ключевые слова in и keyof в mapped types?

Ответ

keyof T получает объединение ключей типа T, а in используется для итерации по ним в mapped type:

type Nullable<T> = {
[P in keyof T]: T[P] | null;
};

Вопрос

Что делает утилита Partial<T>?

Ответ

Partial<T> делает все свойства интерфейса необязательными:

interface User { id: number; name: string; }
type PartialUser = Partial<User>; // { id?: number; name?: string; }

Вопрос

Что делает утилита Required<T>?

Ответ

Required<T> делает все свойства обязательными:

interface Config { theme?: string; timeout?: number; }
type FullConfig = Required<Config>; // { theme: string; timeout: number; }

Вопрос

Что делает утилита Pick<T, K>?

Ответ

Pick<T, K> создаёт новый тип, выбирая только указанные свойства из T:

interface Article { title: string; content: string; author: string; }
type Preview = Pick<Article, "title" | "author">;

Вопрос

Что делает утилита Omit<T, K>?

Ответ

Omit<T, K> создаёт новый тип, исключая указанные свойства:

type PublicUser = Omit<User, "password">;

Вопрос

Как создать свой утилитарный тип?

Ответ

Любой mapped или условный тип может быть оформлен как переиспользуемый утилитарный тип:

type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};

Вопрос

Что такое инференция в условных типах (infer)?

Ответ

Ключевое слово infer позволяет выводить тип внутри условного выражения:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type F = () => string;
type R = ReturnType<F>; // string

Вопрос

Как получить тип элемента массива?

Ответ

Используется условный тип с infer:

type ElementType<T> = T extends (infer E)[] ? E : never;

type T = ElementType<number[]>; // number

Вопрос

Можно ли комбинировать дженерики с перегрузкой функций?

Ответ

Да, но перегрузка должна быть совместима с обобщённой реализацией:

function process<T>(input: T[]): T[];
function process<T>(input: T): T;
function process<T>(input: T | T[]): T | T[] {
return input;
}

Вопрос

Как ограничить дженерик только примитивными типами?

Ответ

Используется объединение примитивов в ограничении:

type Primitive = string | number | boolean | symbol | null | undefined;

function handlePrimitive<T extends Primitive>(value: T): T {
return value;
}

Вопрос

Что такое рекурсивные условные типы?

Ответ

Рекурсивные условные типы ссылаются на самих себя для обработки вложенных структур:

type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? T[P] extends Function
? T[P]
: DeepReadonly<T[P]>
: T[P];
};

Массивы, кортежи и продвинутая типизация

Вопрос

Как объявить типизированный массив?

Ответ

Тип массива указывается двумя способами: через Type[] или через обобщённый тип Array<Type>:

let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ["a", "b"];

Вопрос

Что такое кортеж (tuple)?

Ответ

Кортеж — это массив фиксированной длины, где каждый элемент имеет строго определённый тип:

let person: [string, number] = ["Alice", 30];

Вопрос

Можно ли изменять значения в кортеже?

Ответ

Да, элементы кортежа можно изменять, если они не помечены как readonly. Однако нельзя присвоить значение, не соответствующее типу позиции.

let t: [string, number] = ["hi", 10];
t[0] = "hello"; // OK
t[1] = "no"; // Ошибка

Вопрос

Как объявить readonly-кортеж?

Ответ

Кортеж помечается как readonly, чтобы запретить любые изменения:

const point: readonly [number, number] = [10, 20];
// point[0] = 5; // Ошибка

Вопрос

Что такое rest-элементы в кортежах?

Ответ

Rest-элементы позволяют описать кортеж с переменным хвостом, начиная с определённой позиции:

type NameAndScores = [string, ...number[]];
const data: NameAndScores = ["Alice", 95, 87, 92];

Вопрос

Как получить тип элемента массива?

Ответ

Используется индексный доступ к типу массива:

type Arr = string[];
type Item = Arr[number]; // string

Вопрос

Что делает оператор keyof?

Ответ

Оператор keyof возвращает объединение строковых литералов, соответствующих ключам объектного типа:

interface User { name: string; age: number; }
type Keys = keyof User; // "name" | "age"

Вопрос

Что делает оператор typeof в контексте типов?

Ответ

Оператор typeof получает тип значения во время компиляции:

const config = { timeout: 5000, retries: 3 };
type ConfigType = typeof config; // { timeout: number; retries: number; }

Вопрос

Как использовать keyof и typeof вместе?

Ответ

Эта комбинация позволяет безопасно работать с ключами объектов:

const COLORS = { red: "#f00", green: "#0f0" } as const;
type ColorName = keyof typeof COLORS; // "red" | "green"

Вопрос

Что такое branded types (типы-метки)?

Ответ

Branded types — это паттерн, при котором к типу добавляется уникальное свойство-метка для предотвращения случайной подстановки совместимых, но логически разных типов:

type UserId = string & { __brand: "UserId" };
const createUserId = (id: string): UserId => id as UserId;

Вопрос

Как реализовать тип, который запрещает дополнительные свойства?

Ответ

Используется точный объектный тип с индексной сигнатурой, возвращающей never:

type Exact<T> = T & { [K in Exclude<keyof any, keyof T>]?: never };

interface User { id: number; name: string; }
const u: Exact<User> = { id: 1, name: "A", email: "x" }; // Ошибка: email лишнее

Вопрос

Что такое discriminated unions (размеченные объединения)?

Ответ

Размеченные объединения — это объединение объектных типов, имеющих общее свойство-дискриминатор, по которому TypeScript может точно определить текущий тип:

type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Shape = Circle | Square;

function area(s: Shape): number {
if (s.kind === "circle") return Math.PI * s.radius ** 2;
return s.side ** 2;
}

Вопрос

Как проверить тип внутри размеченного объединения?

Ответ

Проверка выполняется через чтение дискриминатора (например, kind) в условии if или switch. TypeScript автоматически сузит тип.


Вопрос

Что такое type guards (предикаты типов)?

Ответ

Type guard — это функция, которая возвращает булево значение и использует утверждение типа в виде value is Type, чтобы сообщить компилятору о сужении типа:

function isString(value: unknown): value is string {
return typeof value === "string";
}

if (isString(x)) {
x.toUpperCase(); // TypeScript знает, что x — string
}

Вопрос

Как создать пользовательский type guard для класса?

Ответ

Используется instanceof внутри функции с утверждением типа:

class Animal { /* ... */ }

function isAnimal(obj: unknown): obj is Animal {
return obj instanceof Animal;
}

Вопрос

Что такое assertion functions?

Ответ

Assertion function — это функция, которая выбрасывает исключение, если условие не выполнено, и сообщает компилятору, что после её вызова тип гарантированно соответствует ожидаемому:

function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") throw new Error("Not a string");
}

assertIsString(input);
input.toUpperCase(); // OK

Вопрос

Как объявить тип функции с перегрузкой?

Ответ

Перегрузка объявляется несколькими сигнатурами перед реализацией:

function combine(a: string, b: string): string;
function combine(a: number, b: number): number;
function combine(a: string | number, b: string | number): string | number {
return a + b;
}

Вопрос

Можно ли использовать дженерики в перегруженных функциях?

Ответ

Да, но все перегрузки должны быть совместимы с обобщённой реализацией, и вывод типов должен работать корректно.


Вопрос

Что такое callable types?

Ответ

Callable type — это тип, описывающий объект, который можно вызывать как функцию:

interface Add {
(x: number, y: number): number;
}

const add: Add = (a, b) => a + b;

Вопрос

Что такое constructable types?

Ответ

Constructable type — это тип, описывающий класс или конструктор:

interface Constructor {
new (name: string): { name: string };
}

function createInstance(C: Constructor, name: string) {
return new C(name);
}

Вопрос

Как описать функцию, которая возвращает промис?

Ответ

Возвращаемый тип указывается как Promise<T>:

async function fetchData(): Promise<string> {
return "data";
}

// Или без async:
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

Вопрос

Как типизировать then-цепочку промисов?

Ответ

TypeScript автоматически выводит тип результата на основе возвращаемого значения в .then():

fetchData()
.then(data => data.length) // Promise<number>
.then(len => console.log(len));

Вопрос

Что такое this-параметр в функциях?

Ответ

this-параметр явно указывает контекст вызова функции и используется для строгой типизации методов:

interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}

Вопрос

Как запретить использование this в функции?

Ответ

Указывается this: void в параметрах функции:

function pureFunction(this: void, x: number): number {
return x * 2;
}

Вопрос

Что такое contextual typing?

Ответ

Contextual typing — это механизм, при котором TypeScript выводит тип параметра функции на основе ожидаемого контекста:

window.addEventListener("click", (e) => {
e.clientX; // TypeScript знает, что e — MouseEvent
});

Модули, пространства имён и декларации типов

Вопрос

Что такое модуль в TypeScript?

Ответ

Модуль — это файл, содержащий хотя бы один оператор import или export. Модули имеют собственную область видимости и не загрязняют глобальное пространство имён.

// math.ts
export const PI = 3.14159;
export function add(a: number, b: number): number { return a + b; }

Вопрос

Как импортировать значения из модуля?

Ответ

Используются различные формы синтаксиса import:

import { PI, add } from "./math";
import * as MathUtils from "./math";
import addDefault from "./defaultExport";

Вопрос

Что такое default-экспорт?

Ответ

Default-экспорт — это единственный экспорт по умолчанию из модуля, который может быть импортирован под любым именем:

// logger.ts
export default function log(msg: string) { console.log(msg); }

// main.ts
import log from "./logger";

Вопрос

Можно ли совмещать default- и именованные экспорт в одном файле?

Ответ

Да, модуль может содержать один default-экспорт и любое количество именованных экспортов:

// utils.ts
export default function helper() {}
export const constant = 42;
export interface Config { /* ... */ }

Вопрос

Что такое re-export?

Ответ

Re-export позволяет экспортировать значения из другого модуля, не импортируя их в текущую область видимости:

// index.ts
export { add } from "./math";
export { Logger } from "./logger";

Вопрос

Что такое пространство имён (namespace)?

Ответ

Пространство имён — это способ группировки связанных функций, классов и переменных внутри глобальной области видимости с помощью ключевого слова namespace.

namespace Geometry {
export const PI = 3.14;
export function circleArea(r: number): number {
return PI * r * r;
}
}

Вопрос

Когда следует использовать модули, а когда — пространства имён?

Ответ

Модули предпочтительны в современных проектах, особенно при использовании сборщиков (Webpack, Vite). Пространства имён применяются в глобальных скриптах без системы модулей или при написании деклараций типов.


Вопрос

Что такое ambient-модули?

Ответ

Ambient-модули описывают существующие JavaScript-библиотеки, не имеющие собственных типов. Они объявляются с помощью declare module:

declare module "some-library" {
export function doSomething(): void;
}

Вопрос

Что такое declaration files (.d.ts)?

Ответ

Файлы с расширением .d.ts содержат только информацию о типах и не генерируют JavaScript-код. Они используются для описания API внешних библиотек или глобальных переменных.


Вопрос

Как создать файл декларации для своей библиотеки?

Ответ

Добавляется опция "declaration": true в tsconfig.json. При компиляции TypeScript автоматически генерирует .d.ts-файлы рядом с .js.


Вопрос

Что такое DefinitelyTyped?

Ответ

DefinitelyTyped — это репозиторий на GitHub, содержащий высококачественные декларации типов для тысяч популярных JavaScript-библиотек. Пакеты публикуются в npm под префиксом @types/.


Вопрос

Как установить типы для сторонней библиотеки?

Ответ

Типы устанавливаются через npm как @types/имя_пакета:

npm install --save-dev @types/lodash

После этого можно использовать import _ from "lodash" с полной типизацией.


Вопрос

Что делать, если типы для библиотеки отсутствуют в DefinitelyTyped?

Ответ

Создаётся собственный файл декларации (например, custom-lib.d.ts) с описанием API библиотеки с помощью declare module.


Вопрос

Как описать глобальную переменную?

Ответ

Глобальная переменная описывается с помощью declare var, declare let или declare const вне модуля:

// global.d.ts
declare const VERSION: string;

Вопрос

Что такое triple-slash directives?

Ответ

Triple-slash directives — это односторонние комментарии в начале файла, используемые для указания зависимостей типов:

/// <reference types="node" />
/// <reference path="./custom-types.d.ts" />

Вопрос

Когда используется /// <reference types="..." />?

Ответ

Эта директива подключает декларации типов из пакета @types в файлах, не являющихся модулями (например, в глобальных скриптах).


Вопрос

Как описать класс из внешней библиотеки?

Ответ

Используется declare class внутри ambient-модуля или глобального объявления:

declare class Moment {
format(template: string): string;
add(value: number, unit: string): this;
}

Вопрос

Что такое global в декларациях?

Ответ

Ключевое слово global внутри файла декларации позволяет расширять глобальное пространство имён:

declare global {
interface Window {
myApp: any;
}
}

Вопрос

Как расширить интерфейс из внешней библиотеки?

Ответ

Объявляется интерфейс с тем же именем в своём коде. TypeScript объединит все объявления:

// Расширение Node.js Buffer
interface Buffer {
toHex(): string;
}

Вопрос

Можно ли импортировать типы без побочных эффектов?

Ответ

Да, с помощью import type:

import type { User } from "./models";

Этот синтаксис гарантирует, что импорт будет удалён во время компиляции и не повлияет на выполнение.


Вопрос

Что делает флаг isolatedModules?

Ответ

Флаг isolatedModules требует, чтобы каждый файл мог быть транспилирован независимо. Это необходимо при использовании некоторых инструментов, таких как Babel.


Вопрос

Как экспортировать типы вместе с значениями?

Ответ

Типы можно экспортировать отдельно или вместе с значениями:

export interface Config { /* ... */ }
export const config: Config = { /* ... */ };

Вопрос

Что такое barrel-файл?

Ответ

Barrel-файл — это файл (часто index.ts), который переэкспортирует члены из других модулей, упрощая импорт для потребителей:

// lib/index.ts
export { UserService } from "./user";
export { AuthService } from "./auth";

Вопрос

Как предотвратить импорт внутренних частей модуля?

Ответ

Внутренние части не экспортируются. Доступ возможен только к явно экспортированным членам.


Вопрос

Что такое synthetic default imports?

Ответ

Synthetic default imports — это возможность импортировать CommonJS-модуль как default-импорт, даже если он его не предоставляет. Включается флагом "allowSyntheticDefaultImports": true.


Конфигурация TypeScript и продвинутые настройки

Вопрос

Что такое tsconfig.json?

Ответ

tsconfig.json — это файл конфигурации проекта TypeScript, который определяет параметры компиляции, включает или исключает файлы, задаёт целевую версию JavaScript и управляет поведением системы типов.


Вопрос

Какие обязательные поля есть в tsconfig.json?

Ответ

Файл может быть пустым ({}), но обычно содержит секцию "compilerOptions". Поля "files", "include" и "exclude" необязательны.


Вопрос

Что делает опция "target"?

Ответ

Опция "target" указывает, в какую версию ECMAScript будет скомпилирован код. Например, "target": "ES2020" сохранит современные конструкции, а "target": "ES5" преобразует их в совместимый код.


Вопрос

Что делает опция "module"?

Ответ

Опция "module" задаёт систему модулей для генерируемого JavaScript: "commonjs", "es2015", "esnext", "amd", "umd" и другие.


Вопрос

Что такое "strict" в tsconfig.json?

Ответ

Флаг "strict": true включает набор строгих проверок типов, включая strictNullChecks, strictFunctionTypes, noImplicitAny, alwaysStrict и другие. Это рекомендуемый режим для новых проектов.


Вопрос

Что делает "strictNullChecks"?

Ответ

При включённом "strictNullChecks" значения null и undefined не являются частью других типов. Переменная типа string не может содержать null, если явно не указано string | null.


Вопрос

Что делает "noImplicitAny"?

Ответ

Флаг "noImplicitAny" запрещает вывод типа any, когда TypeScript не может определить конкретный тип. Это заставляет разработчика явно указывать типы.


Вопрос

Что делает "noUnusedLocals" и "noUnusedParameters"?

Ответ

Эти флаги вызывают ошибку компиляции при наличии неиспользуемых локальных переменных или параметров функций, что помогает поддерживать чистоту кода.


Вопрос

Как указать, какие файлы включать в компиляцию?

Ответ

Используется поле "include" с массивом шаблонов glob:

{
"include": ["src/**/*", "types/**/*"]
}

Вопрос

Как исключить файлы из компиляции?

Ответ

Используется поле "exclude":

{
"exclude": ["node_modules", "dist", "**/*.spec.ts"]
}

По умолчанию node_modules и bower_components исключены автоматически.


Вопрос

Что такое "baseUrl" и "paths"?

Ответ

"baseUrl" задаёт корень для непрямых импортов. "paths" позволяет создавать псевдонимы для путей:

{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@utils/*": ["src/utils/*"],
"@components/*": ["src/components/*"]
}
}
}

Вопрос

Что делает "outDir"?

Ответ

Опция "outDir" указывает директорию, в которую будут помещены скомпилированные .js-файлы:

{ "outDir": "./dist" }

Вопрос

Что такое "declaration"?

Ответ

Флаг "declaration": true указывает компилятору генерировать файлы деклараций (.d.ts) вместе с .js-файлами, что необходимо для публикации библиотек.


Вопрос

Что делает "sourceMap"?

Ответ

Флаг "sourceMap": true включает генерацию .map-файлов, которые позволяют отладчикам показывать исходный TypeScript-код вместо скомпилированного JavaScript.


Вопрос

Что такое "esModuleInterop"?

Ответ

Флаг "esModuleInterop" улучшает совместимость между CommonJS и ES-модулями, автоматически создавая default-экспорты для CommonJS-модулей. Это позволяет использовать import React from "react" даже если библиотека использует CommonJS.


Вопрос

Что делает "allowSyntheticDefaultImports"?

Ответ

Флаг "allowSyntheticDefaultImports" позволяет импортировать не-ES-модули как default-импорты без ошибок типизации. Он влияет только на проверку типов, а не на генерируемый код.


Вопрос

Что такое "skipLibCheck"?

Ответ

Флаг "skipLibCheck": true отключает проверку типов в файлах деклараций (.d.ts), что ускоряет компиляцию и избегает ошибок в сторонних типах.


Вопрос

Что делает "forceConsistentCasingInFileNames"?

Ответ

Этот флаг требует, чтобы все импорты использовали точное соответствие регистра имён файлов, что предотвращает ошибки на файловых системах, чувствительных к регистру (например, Linux).


Вопрос

Как настроить разные конфигурации для разных окружений?

Ответ

Используются расширения конфигурации через "extends":

// tsconfig.base.json
{ "compilerOptions": { /* общие настройки */ } }

// tsconfig.prod.json
{ "extends": "./tsconfig.base.json", "compilerOptions": { "sourceMap": false } }

Вопрос

Что такое "composite"?

Ответ

Флаг "composite": true включает поддержку проектных ссылок (Project References), позволяя разбивать большой проект на модули с зависимостями и ускорять инкрементальную сборку.


Вопрос

Что такое Project References?

Ответ

Project References — это механизм, при котором один TypeScript-проект ссылается на другой через "references" в tsconfig.json. Это улучшает производительность и изоляцию.

{
"references": [{ "path": "../shared" }]
}

Вопрос

Как включить проверку типов без генерации файлов?

Ответ

Используется флаг --noEmit в командной строке или "noEmit": true в tsconfig.json. Это полезно для CI/CD или проверки типов в режиме «только анализ».


Вопрос

Что делает "resolveJsonModule"?

Ответ

Флаг "resolveJsonModule": true позволяет импортировать .json-файлы как модули с автоматическим выводом их типа:

import config from "./settings.json"; // Тип выводится из содержимого JSON

Вопрос

Что такое "incremental"?

Ответ

Флаг "incremental": true включает инкрементальную компиляцию. TypeScript сохраняет информацию о предыдущей сборке в файле .tsbuildinfo и пересобирает только изменённые части.


Вопрос

Как проверить, что конфигурация корректна?

Ответ

Выполняется команда tsc --noEmit или tsc --watch для наблюдения за ошибками в реальном времени. Также можно использовать tsc --listFiles, чтобы увидеть, какие файлы включены в компиляцию.


Практические сценарии и продвинутая типизация

Вопрос

Как типизировать объект с динамическими ключами, значения которых зависят от ключа?

Ответ

Используется mapped type с условным типом или функция-конструктор:

type EventHandlerMap = {
[K in EventName]: (payload: PayloadFor<K>) => void;
};

Где EventName — объединение строк, а PayloadFor<T> — условный тип, возвращающий нужный payload.


Вопрос

Как реализовать типобезопасный EventEmitter?

Ответ

Создаётся класс с обобщённым методом on, где имя события и тип данных связаны через интерфейс маппинга:

interface Events {
data: string;
error: Error;
}

class EventEmitter {
on<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void): void {
// реализация подписки
}
}

Вопрос

Как типизировать API-клиент с разными эндпоинтами и ответами?

Ответ

Описывается маппинг путей к типам ответов, и метод fetch использует generic-ограничение:

interface ApiSchema {
"/user": { id: number; name: string };
"/posts": { id: number; title: string }[];
}

async function apiGet<P extends keyof ApiSchema>(path: P): Promise<ApiSchema[P]> {
const res = await fetch(path);
return res.json();
}

Вопрос

Как обеспечить неизменяемость состояния в приложении?

Ответ

Используются readonly-типы, утилиты вроде Readonly<T>, DeepReadonly<T>, и запрет мутаций через ESLint-правила или строгую архитектуру (например, Redux с иммерсией).


Вопрос

Как типизировать форму с валидацией?

Ответ

Создаётся интерфейс формы, и каждое поле связывается с правилом валидации. Ошибки представляются как частичный объект:

interface FormData { email: string; age: number; }
type FormErrors = Partial<{ [K in keyof FormData]: string }>;

Вопрос

Как работать с опциональными полями при отправке данных на сервер?

Ответ

Используется утилита Required<T> или создание отдельного типа для запроса, исключающего undefined:

type SubmitData = {
[K in keyof FormData as FormData[K] extends undefined ? never : K]: FormData[K];
};

Вопрос

Как типизировать хук React useState с начальным значением null?

Ответ

Указывается объединённый тип:

const [user, setUser] = useState<User | null>(null);

Это гарантирует, что компонент корректно обрабатывает состояние «данные ещё не загружены».


Вопрос

Как типизировать контекст React?

Ответ

Тип контекста задаётся при создании:

interface AppContextType { theme: string; toggleTheme(): void; }
const AppContext = createContext<AppContextType | null>(null);

Потребители проверяют наличие значения или используют обёртку-хелпер.


Вопрос

Как типизировать компонент высшего порядка (HOC)?

Ответ

HOC описывается как generic-функция, сохраняющая пропсы оригинального компонента:

function withAuth<P extends object>(
Component: React.ComponentType<P>
): React.ComponentType<P> {
return (props) => {
const isAuthenticated = useAuth();
return isAuthenticated ? <Component {...props} /> : <Login />;
};
}

Вопрос

Как типизировать пользовательский хук?

Ответ

Возвращаемое значение хука описывается явно или выводится автоматически. Рекомендуется указывать возвращаемый тип для сложных хуков:

function useCounter(initial: number): [number, () => void] {
const [count, setCount] = useState(initial);
const increment = () => setCount(c => c + 1);
return [count, increment];
}

Вопрос

Как обеспечить типобезопасность при работе с localStorage?

Ответ

Создаётся обёртка с сериализацией и десериализацией, параметризованная типом:

function saveToStorage<T>(key: string, value: T): void {
localStorage.setItem(key, JSON.stringify(value));
}

function loadFromStorage<T>(key: string, defaultValue: T): T {
const item = localStorage.getItem(key);
if (!item) return defaultValue;
try {
return JSON.parse(item) as T;
} catch {
return defaultValue;
}
}

Вопрос

Как типизировать маршруты в SPA?

Ответ

Описывается перечисление путей и маппинг к параметрам:

interface RouteParams {
"/user/:id": { id: string };
"/search": { q?: string };
}

type RoutePath = keyof RouteParams;

Вопрос

Как реализовать типобезопасный роутер?

Ответ

Метод навигации принимает путь и объект параметров, совместимых с маппингом:

function navigate<P extends RoutePath>(path: P, params: RouteParams[P]): void {
// подстановка параметров в URL
}

Вопрос

Как работать с полиморфными компонентами в UI-библиотеках?

Ответ

Компонент принимает generic-параметр as, определяющий тег или другой компонент, и проксирует пропсы:

interface BoxProps<T extends React.ElementType> {
as?: T;
children: React.ReactNode;
}

function Box<T extends React.ElementType = "div">({
as: Tag = "div",
children,
...props
}: BoxProps<T> & Omit<React.ComponentProps<T>, keyof BoxProps<T>>) {
return <Tag {...props}>{children}</Tag>;
}

Вопрос

Как типизировать конфигурацию плагина?

Ответ

Используется интерфейс с необязательными полями и значениями по умолчанию в реализации:

interface PluginOptions {
timeout?: number;
retries?: number;
logger?: (msg: string) => void;
}

Вопрос

Как предотвратить ошибки при работе с API, возвращающим any?

Ответ

Оборачивается вызов в функцию с явной декодирующей логикой и проверкой структуры:

function decodeUser(data: unknown): User {
if (typeof data === "object" && data && "id" in data && "name" in data) {
return data as User;
}
throw new Error("Invalid user data");
}

Вопрос

Как использовать Zod или io-ts вместе с TypeScript?

Ответ

Эти библиотеки предоставляют runtime-валидацию и генерируют типы TypeScript из схем. Это сочетает статическую и динамическую типизацию:

import { z } from "zod";
const UserSchema = z.object({ id: z.number(), name: z.string() });
type User = z.infer<typeof UserSchema>;

Вопрос

Как типизировать middleware в Express?

Ответ

Middleware описывается с учётом расширения объекта Request:

declare global {
namespace Express {
interface Request {
user?: User;
}
}
}

const authMiddleware: RequestHandler = (req, res, next) => {
req.user = getUserFromToken(req.headers.authorization);
next();
};

Вопрос

Как типизировать GraphQL-запросы?

Ответ

Используются codegen-инструменты (например, GraphQL Code Generator), которые генерируют TypeScript-типы на основе схемы и операций.


Вопрос

Как обеспечить согласованность типов между клиентом и сервером?

Ответ

Общие типы выносятся в отдельный пакет или shared-директорию, доступную и клиенту, и серверу. Альтернатива — генерация типов из OpenAPI/Swagger или GraphQL-схемы.


Вопрос

Как типизировать события в DOM?

Ответ

TypeScript предоставляет встроенные типы событий: MouseEvent, KeyboardEvent, ChangeEvent<HTMLInputElement> и другие. Они используются в обработчиках:

function handleClick(e: MouseEvent) { /* ... */ }

Вопрос

Как работать с нестандартными свойствами элементов (data-атрибуты)?

Ответ

Используется индексная сигнатура или расширение интерфейса:

interface HTMLElement {
dataset: DOMStringMap;
}
// dataset уже встроен, но можно добавить кастомные свойства при необходимости

Вопрос

Как типизировать Web Workers?

Ответ

Создаётся отдельный файл с типизированными сообщениями и используется postMessage с известной структурой:

// worker.ts
self.onmessage = (e: MessageEvent<{ action: "process"; data: number[] }>) => {
// обработка
};

Вопрос

Как типизировать Service Workers и Cache API?

Ответ

Используются встроенные глобальные типы (ServiceWorkerGlobalScope, Cache), предоставляемые lib.dom.d.ts. Дополнительно можно расширить интерфейсы при кастомной логике.


Вопрос

Как обеспечить типобезопасность при использовании Proxy?

Ответ

Proxy оборачивается в функцию, которая принимает целевой объект и обработчик, типизированный относительно этого объекта:

function createLoggedProxy<T extends object>(target: T): T {
return new Proxy(target, {
get(obj, prop) {
console.log(`Getting ${String(prop)}`);
return obj[prop as keyof T];
}
});
}

Экспертные темы и метапрограммирование типов

Вопрос

Как реализовать тип, который вычисляет длину кортежа на уровне типов?

Ответ

Используется рекурсивный условный тип с инференцией:

type Length<T extends any[]> = T["length"];

// Или через рекурсию (для демонстрации):
type TupleLength<T extends unknown[]> = T extends [infer _, ...infer Rest]
? 1 extends number
? Succ<TupleLength<Rest>>
: never
: 0;

// Где Succ — это представление чисел через вложенные кортежи или литералы (в современных версиях можно использовать number literal inference).

В реальных сценариях достаточно T["length"], так как TypeScript хранит длину кортежа как числовой литерал.


Вопрос

Как реализовать типобезопасную функцию zip для двух массивов?

Ответ

Создаётся generic-функция, принимающая два кортежа одинаковой длины и возвращающая кортеж пар:

type Zip<A extends any[], B extends any[]> = A extends [infer AH, ...infer AR]
? B extends [infer BH, ...infer BR]
? [[AH, BH], ...Zip<AR, BR>]
: []
: [];

function zip<A extends any[], B extends any[]>(
a: A,
b: B
): Zip<A, B> {
const result: any[] = [];
for (let i = 0; i < Math.min(a.length, b.length); i++) {
result.push([a[i], b[i]]);
}
return result as Zip<A, B>;
}

Это работает корректно только при известной длине (например, для кортежей).


Вопрос

Что такое variance в системе типов TypeScript?

Ответ

Variance описывает, как отношения подтипности между составными типами соотносятся с отношениями их компонентов. TypeScript использует структурную типизацию и в большинстве случаев применяет ковариантное поведение для свойств объектов и параметров функций (с ограничениями при строгой проверке функций).


Вопрос

Почему Array<string> не является подтипом Array<string | number> при строгой типизации?

Ответ

Потому что массивы в TypeScript считаются изменяемыми. Если бы Array<string> был подтипом Array<string | number>, можно было бы добавить число в массив строк, нарушая безопасность. Поэтому массивы ведут себя инвариантно при наличии мутаций.


Вопрос

Как оптимизировать производительность системы типов в крупном проекте?

Ответ

Применяются следующие подходы:
— Разбиение проекта на модули с Project References;
— Использование "skipLibCheck": true;
— Ограничение глубины рекурсии типов через контроль над mapped и conditional types;
— Избегание чрезмерно сложных утилитарных типов в горячих путях кода;
— Применение "incremental": true и кэширование .tsbuildinfo;
— Минимизация использования any и unknown без последующего сужения.