Skip to content

TypeScript Шпаргалка

Типизированное надмножество JavaScript, которое масштабируется.

01

Начало работы

Hello World и компиляция

Файлы TypeScript используют расширение .ts. Компилятор tsc транслирует TS в JS, стирая все аннотации типов во время выполнения. Используйте --strict для максимальной type safety. ts-node или bun могут запускать .ts файлы напрямую без отдельного этапа компиляции.

typescript
// hello.ts
const message: string = "Hello, TypeScript!";
console.log(message);

// Compile to JavaScript (type annotations erased):
//   tsc hello.ts          -> hello.js
//   tsc --strict hello.ts  // enable all strict checks
//   tsc --watch hello.ts   // recompile on change

// Run directly with ts-node or bun:
//   ts-node hello.ts

tsconfig.json

tsconfig.json настраивает компилятор TypeScript. 'strict: true' включает noImplicitAny, strictNullChecks, strictFunctionTypes и многое другое. 'target' управляет выходной версией JS. 'esModuleInterop' включает импорт по умолчанию из модулей CommonJS, таких как встроенные модули Node.

typescript
{
  "compilerOptions": {
    "target": "ES2020",        // JS version to emit
    "module": "ESNext",        // module system
    "strict": true,            // enable all strict checks
    "outDir": "./dist",        // output directory
    "rootDir": "./src",        // source root
    "esModuleInterop": true,   // allow default imports from CJS
    "skipLibCheck": true,      // skip .d.ts type checking
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Аннотации типов и вывод типов

Аннотации типов явно указывают тип переменной. TypeScript также может выводить типы из значений. Используйте явные аннотации для сигнатур функций и публичных API; полагайтесь на вывод типов для локальных переменных. Избегайте 'any' — это полностью отключает проверку типов.

typescript
// Explicit type annotations
let count: number = 0;
const name: string = "Alice";
let isDone: boolean = true;
let ids: number[] = [1, 2, 3];
let tuple: [string, number] = ["age", 30];

// Type inference (let TS figure it out)
let age = 30;           // inferred as number
let items = [1, 2, 3];  // inferred as number[]
let mixed = [1, "a"];   // inferred as (string | number)[]

// any disables type checking (avoid!)
let anything: any = 42;
anything = "string";    // no error

Проверки строгого режима

Строгий режим включает критически важные проверки: strictNullChecks (null/undefined нельзя присвоить другим типам), noImplicitAny (параметры должны иметь типы), strictPropertyInitialization (поля класса должны быть инициализированы). Используйте '!' (определённое присваивание), когда вы уверены, что поле будет установлено позже.

typescript
// strictNullChecks: null/undefined not assignable to other types
let n: string = null; // Error in strict mode

// noImplicitAny: must annotate parameters
function greet(name) { } // Error: implicit any

// strictPropertyInitialization
class User {
  name: string; // Error: not initialized
  constructor() {}
}

// Fix: initialize or use definite assignment
class Fixed {
  name!: string; // definite assignment assertion
  age: number = 0;
}

Файлы объявлений (.d.ts)

Файлы объявлений (.d.ts) предоставляют типы для библиотек JavaScript без определений TypeScript. 'declare' сообщает компилятору, что переменная/функция существует во время выполнения. Используйте пакеты @types из DefinitelyTyped для популярных библиотек (например, @types/node, @types/react).

typescript
// types.d.ts - type declarations only, no implementation
declare module "my-lib" {
  export function greet(name: string): string;
  export const version: string;
}

// global.d.ts - extend global scope
declare global {
  interface Window {
    myApp: { version: string };
  }
}

// Usage in .ts files:
// import { greet } from "my-lib";  // now typed
// Install @types packages: npm i -D @types/node @types/react
02

Базовые типы

Примитивы и специальные типы

Примитивы TypeScript: string, number, boolean, bigint, symbol. 'void' указывает, что функция не возвращает значение. 'never' представляет значения, которые никогда не возникают — функции, которые выбрасывают исключение или выполняются бесконечно. Используйте 'never' для исчерпывающих проверок в switch.

typescript
let str: string = "hello";
let num: number = 42;
let bool: boolean = true;
let big: bigint = 100n;
let sym: symbol = Symbol("id");

// void: function returns nothing
function log(msg: string): void { console.log(msg); }

// never: function never returns
function fail(msg: string): never { throw new Error(msg); }
function infinite(): never { while (true) {} }

Массивы и кортежи

Массивы используют синтаксис T[] или Array<T>. ReadonlyArray предотвращает мутации. Кортежи — массивы фиксированной длины с определёнными типами на каждой позиции — полезны для пар ключ-значение или CSV-подобных данных. Именованные кортежи улучшают читаемость с помощью названных позиций.

typescript
// Two syntaxes for arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b"];

// ReadonlyArray (immutable)
const ro: ReadonlyArray<number> = [1, 2, 3];
// ro.push(4); // Error: not mutable

// Tuple (fixed length, known types)
let tuple: [string, number] = ["Alice", 30];
let name = tuple[0]; // string
let age = tuple[1];  // number

// Labeled tuple elements (TS 4.0+)
let entry: [name: string, age: number] = ["Bob", 25];

Enum'ы

Enum'ы определяют набор именованных констант. Строковые enum'ы рекомендуются для отладки (значения читаемы в выводе). Числовые enum'ы поддерживают обратное отображение. 'const enum' удаляется при компиляции (встраивается) без накладных расходов во время выполнения. Для простых случаев предпочитайте типы объединения.

typescript
// Numeric enum (default starts at 0)
enum Direction { Up, Down, Left, Right }
let d: Direction = Direction.Up;  // 0

// String enum (recommended for readability)
enum Status {
  Pending = "PENDING",
  Success = "SUCCESS",
  Failed = "FAILED",
}

// Reverse mapping (numeric only)
console.log(Direction[0]); // "Up"

// Const enum (inlined, no runtime object)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;

any vs unknown vs never

'any' отключает всю проверку типов — избегайте его. 'unknown' — типобезопасная альтернатива: вы должны сузить его (через typeof, instanceof) перед использованием. 'never' представляет значения, которые никогда не возникают, используется для исчерпывающей проверки в switch для отлова пропущенных случаев при компиляции.

typescript
// any: opt out of type checking (dangerous!)
let a: any = 42;
a = "string";       // OK
a.toUpperCase();    // OK (no check, may fail at runtime)

// unknown: type-safe alternative to any
let u: unknown = 42;
// u.toUpperCase(); // Error: unknown type
if (typeof u === "string") {
  u.toUpperCase();  // OK after narrowing
}

// never: impossible value (exhaustiveness check)
type Shape = "circle" | "square";
function area(s: Shape) {
  switch (s) {
    case "circle": return Math.PI;
    case "square": return 1;
    default:
      const _exhaustive: never = s; // Error if case missing
  }
}

Утверждения типа

Утверждения типа сообщают компилятору 'доверься мне, я знаю тип'. Используйте синтаксис 'as'. Утверждение non-null (!) сообщает TS, что значение не null/undefined. 'as const' делает все свойства readonly-литералами — полезно для объектов конфигурации и типов действий Redux. Утверждения не меняют поведение во время выполнения.

typescript
// as syntax (preferred, works in .tsx)
let val: unknown = "hello";
let len: number = (val as string).length;

// Non-null assertion (!)
let el = document.querySelector("#app")!;
el.innerHTML = "Hi";  // el is HTMLElement, not null

// Double assertion (for unsafe casts)
let value = "42" as unknown as number;

// const assertion (literal types)
const req = { method: "GET", url: "/api" } as const;
// req.method has type "GET" (not string)
// req is readonly

Литеральные типы и типы объединения

Литеральные типы ограничивают значение конкретной строкой, числом или логическим значением. В сочетании с объединениями они создают точные типы вроде direction или HTTP-методов. Template literal типы (TS 4.1+) строят строковые типы из других типов — мощный инструмент для генерации типобезопасных ключей и имён событий.

typescript
// String literal types
let direction: "left" | "right" | "up" | "down";
direction = "left";  // OK
// direction = "sideways"; // Error

// Numeric literal types
let dice: 1 | 2 | 3 | 4 | 5 | 6;
dice = 4;  // OK

// Boolean literal
let flag: true = true;

// Template literal types (TS 4.1+)
type Color = "red" | "blue";
type Size = "small" | "large";
type Variant = `${Size}-${Color}`;
// "small-red" | "small-blue" | "large-red" | "large-blue"
03

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

Основы интерфейсов

Интерфейсы описывают форму объектов. '?' помечает необязательные свойства (могут быть undefined). 'readonly' предотвращает переприсваивание после инициализации. Интерфейсы существуют только на этапе компиляции — они удаляются из выходного JavaScript. Используйте их для определения контрактов для объектов и классов.

typescript
interface User {
  id: number;
  name: string;
  email?: string;        // optional property
  readonly createdAt: Date; // immutable
}

const user: User = {
  id: 1,
  name: "Alice",
  createdAt: new Date(),
};

// user.createdAt = new Date(); // Error: readonly
// user.email; // string | undefined

Индексные сигнатуры

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

typescript
// Index signature: arbitrary string keys
interface StringMap {
  [key: string]: string;
}

const dict: StringMap = {
  name: "Alice",
  city: "NYC",
  // count: 42, // Error: value must be string
};

// Mixed: known + index signature
interface Config {
  name: string;
  [key: string]: string | number;
}

// Readonly index signature
interface ReadonlyMap {
  readonly [key: string]: number;
}

Расширение интерфейсов

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

typescript
interface Animal {
  name: string;
  eat(): void;
}

interface Pet extends Animal {
  owner: string;
  play(): void;
}

interface Swimmer {
  swim(): void;
}

// Multiple inheritance
interface Duck extends Pet, Swimmer {
  quack(): void;
}

const duck: Duck = {
  name: "Donald",
  owner: "Walt",
  eat() {},
  play() {},
  swim() {},
  quack() {},
};

Функциональные типы в интерфейсах

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

typescript
interface SearchFn {
  (source: string, keyword: string): boolean;
}

const contains: SearchFn = (src, kw) => src.includes(kw);
console.log(contains("hello world", "world")); // true

// Interface with mixed members (hybrid)
interface Counter {
  (start: number): void;  // callable
  count: number;           // property
  reset(): void;           // method
}

// Function with properties (jQuery-style)
const counter: any = (n: number) => { counter.count = n; };
counter.count = 0;
counter.reset = () => { counter.count = 0; };

Интерфейс vs Type Alias

Интерфейсы поддерживают слияние объявлений (одноимённые интерфейсы объединяются), лучшие сообщения об ошибках и предпочтительны для форм объектов/классов. Type aliases более гибки (могут представлять объединения, примитивы, кортежи), но не могут объединяться. Используйте интерфейсы для расширяемых API, type aliases — для объединений и вычисляемых типов.

typescript
// Interface: extendable, better error messages
interface Window { title: string; }
interface Window { size: number; } // declaration merging
const w: Window = { title: "App", size: 800 };

// Type alias: more flexible (unions, primitives, etc.)
type ID = string | number;
type Callback<T> = (value: T) => void;

// Both can describe object shapes
interface UserI { name: string; }
type UserT = { name: string; };

// Use interface for objects/classes, type for unions/aliases

Optional Chaining и Nullish Coalescing

Optional chaining (?.) безопасно обращается к вложенным свойствам — возвращает undefined вместо выбрасывания исключения, если любой элемент равен null/undefined. Nullish coalescing (??) предоставляет значение по умолчанию только для null/undefined (не для 0 или ''). Эти операторы значительно сокращают громоздкий код проверки на null.

typescript
interface User {
  profile?: {
    address?: {
      city?: string;
    };
  };
}

const user: User = {};

// Optional chaining (?.) - safe property access
const city = user.profile?.address?.city; // string | undefined

// Nullish coalescing (??) - default value
const name = user.profile?.address?.city ?? "Unknown";

// Non-null assertion (!) - you're sure it's not null
// const c = user.profile!.address!.city!; // risky

// Optional method call
const result = user.profile?.address?.city?.toUpperCase();
04

Type Aliases и Unions

Type Aliases

Type aliases создают именованные ссылки на любой тип, включая объединения, пересечения, примитивы и дженерики. В отличие от интерфейсов, aliases нельзя объединять или расширять, но они более гибки. Используйте aliases для объединений, кортежей и utility типов; интерфейсы — для форм объектов.

typescript
// Basic alias
type ID = string | number;
type Point = { x: number; y: number };

// Generic alias
type Container<T> = { value: T };

// Function type alias
type Handler<T> = (event: T) => void;

// Usage
const id: ID = 42;
const p: Point = { x: 1, y: 2 };
const box: Container<string> = { value: "hi" };
const onClick: Handler<string> = (e) => console.log(e);

Типы объединения (Union)

Типы объединения (A | B) позволяют значению быть одним из нескольких типов. TypeScript сужает тип внутри условных блоков с помощью typeof, instanceof или in. Примечание: (string | number)[] отличается от string[] | number[] — первое это смешанный массив, второе это все-строки ИЛИ все-числа.

typescript
// Union: value can be one of several types
type ID = string | number;

function display(id: ID) {
  if (typeof id === "string") {
    console.log(id.toUpperCase()); // narrowed to string
  } else {
    console.log(id.toFixed(2));    // narrowed to number
  }
}

display("abc");  // ABC
display(42);     // 42.00

// Union of arrays vs array of unions
type Mixed = (string | number)[];
type Either = string[] | number[];

Типы пересечения (Intersection)

Типы пересечения (A & B) объединяют все члены нескольких типов — результат должен удовлетворять каждому типу. Полезно для mixins, композиции и слияния utility типов. В отличие от объединения (OR), пересечение это AND: значение должно иметь все свойства всех типов.

typescript
// Intersection: combine multiple types into one
interface BusinessPartner {
  name: string;
  credit: number;
}

interface Identity {
  id: number;
  email: string;
}

type Employee = BusinessPartner & Identity;

const emp: Employee = {
  name: "Alice",
  credit: 1000,
  id: 1,
  email: "[email protected]",
};

// All properties required
// const bad: Employee = { name: "Bob" }; // Error: missing props

Nullable типы

В строгом режиме null и undefined нельзя присвоить другим типам — вы должны явно включить их через объединения (string | null). Необязательные параметры (param?) неявно T | undefined. Используйте ?? для безопасных значений по умолчанию и ! для утверждения non-null (используйте экономно).

typescript
// In strict mode, null/undefined are separate types
let name: string | null = null;
name = "Alice";  // OK

// Optional parameters are implicitly | undefined
function greet(name?: string) {
  // name is string | undefined
  console.log(name ?? "Guest");
}

// Return type can be null
function find(id: number): string | null {
  return id === 1 ? "Alice" : null;
}

// Non-null assertion
const result = find(1)!.toUpperCase(); // "ALICE"

Операторы Keyof и Typeof

'keyof T' извлекает ключи типа T как строковое литеральное объединение. 'typeof x' извлекает тип значения (полезно для вывода из объектов). 'keyof typeof obj' объединяет оба для получения ключей существующего объекта — распространено в типах действий Redux и типобезопасных аксессорах свойств.

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

// keyof: extract keys as a union
type UserKey = keyof User; // "id" | "name" | "email"

function getProp(obj: User, key: keyof User) {
  return obj[key];
}

// typeof: extract type from a value
const config = { port: 3000, host: "localhost" };
type Config = typeof config; // { port: number; host: string }

// keyof typeof: keys of an object
type ConfigKey = keyof typeof config; // "port" | "host"

Mapped типы

Mapped типы итерируются по ключам для преобразования типа. Встроенные utilities вроде Readonly, Partial и Pick являются mapped типами. Используйте модификаторы + и - для добавления/удаления readonly или optional. Переименование ключей (TS 4.1+) переименовывает ключи с помощью template literal типов — мощный инструмент для генерации типов getter/setter.

typescript
// Map over keys to create a new type
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Optional<T> = {
  [K in keyof T]?: T[K];
};

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

type ReadonlyUser = Readonly<User>;   // all readonly
type OptionalUser = Optional<User>;   // all optional

// Modifiers: +add, -remove
type Mutable<T> = { -readonly [K in keyof T]: T[K]; };
type Required<T> = { [K in keyof T]-?: T[K]; };

// Key remapping (TS 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
05

Функции

Типы функций и сигнатуры

TypeScript добавляет аннотации типов к параметрам функций и возвращаемым значениям. Возвращаемый тип часто можно вывести, но явная аннотация рекомендуется для публичных API. Параметры по умолчанию делают аргументы необязательными со значением по умолчанию. Используйте void, когда функция ничего не возвращает.

typescript
// Named function with types
function add(a: number, b: number): number {
  return a + b;
}

// Arrow function with types
const multiply = (a: number, b: number): number => a * b;

// Function type alias
type MathOp = (a: number, b: number) => number;
const divide: MathOp = (a, b) => a / b;

// Void return (no return value)
function log(msg: string): void { console.log(msg); }

// Optional and default parameters
function greet(name: string, greeting: string = "Hi"): string {
  return `${greeting}, ${name}!`;
}
greet("Alice");        // "Hi, Alice!"
greet("Bob", "Hello"); // "Hello, Bob!"

Rest-параметры и кортежи

Rest-параметры (...args) собирают несколько аргументов в массив. TypeScript типизирует их как T[] или кортеж для вариативных функций фиксированной длины. Spread-оператор (...) делает обратное — разворачивает массив в отдельные аргументы. Rest-типы кортежей обеспечивают точные вариативные сигнатуры.

typescript
// Rest parameters (variadic)
function sum(...nums: number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10

// Tuple rest (fixed prefix + variadic tail)
function pair(name: string, ...scores: number[]): void {
  console.log(name, scores);
}

// Spread call
const nums = [1, 2, 3];
console.log(sum(...nums));

// Typed rest as tuple
function f(...args: [string, number, boolean]): void {
  const [s, n, b] = args;
}

Перегрузки функций

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

typescript
// Overload signatures (what callers see)
function parse(input: string): string[];
function parse(input: number): number[];
// Implementation signature (not visible to callers)
function parse(input: string | number): string[] | number[] {
  if (typeof input === "string") {
    return input.split(",");
  }
  return [input, input * 2];
}

const strs = parse("a,b,c"); // string[]
const nums = parse(42);      // number[]

// Overloads with different param counts
function makeDate(timestamp: number): Date;
function makeDate(y: number, m: number, d: number): Date;
function makeDate(yOrTs: number, m?: number, d?: number): Date {
  return m === undefined
    ? new Date(yOrTs)
    : new Date(yOrTs, m - 1, d);
}

Тип this

TypeScript позволяет объявить тип 'this' как первый параметр. Это гарантирует, что функция вызывается с правильным контекстом — полезно для методов, передаваемых как обратные вызовы. Стрелочные функции захватывают 'this' лексически, избегая необходимости в .bind(). Используйте 'noImplicitThis' для отлова нетипизированных ошибок 'this'.

typescript
interface Card {
  suit: string;
  rank: string;
  isFaceUp(): boolean;
}

// Explicit 'this' parameter
function format(this: Card): string {
  return `${this.rank} of ${this.suit}`;
}

const card: Card = {
  suit: "Hearts",
  rank: "A",
  isFaceUp() { return true; },
  format,
};

// 'this' in callbacks with bind
class Handler {
  private count = 0;
  increment = () => { this.count++; }; // arrow binds this
}

Обратные вызовы и функции высшего порядка

TypeScript полностью типизирует функции высшего порядка (функции, принимающие или возвращающие функции). Используйте параметры дженериков (T, U) для сохранения типовых отношений между входом и выходом. Типы обратных вызовов обычно определяются как type aliases для повторного использования. Каррирование (возврат функций) полностью типобезопасно.

typescript
// Function as parameter
type Callback<T> = (value: T, index: number) => void;

function forEach<T>(arr: T[], cb: Callback<T>): void {
  for (let i = 0; i < arr.length; i++) {
    cb(arr[i], i);
  }
}

forEach(["a", "b"], (v, i) => console.log(i, v));

// Function returning function (curry)
function add(a: number): (b: number) => number {
  return (b) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8

// Generic map
function map<T, U>(arr: T[], fn: (x: T) => U): U[] {
  return arr.map(fn);
}

Деструктуризация параметров

TypeScript поддерживает деструктуризацию в параметрах функций — аннотируйте деструктурированную форму инлайн или через интерфейс. Извлечение в интерфейс улучшает читаемость и повторное использование. Деструктуризация массивов/кортежей также работает. Этот паттерн распространён в props React-компонентов и обработчиках API.

typescript
// Destructured parameters with types
function createUser({ name, age, email }: {
  name: string;
  age: number;
  email?: string;
}): void {
  console.log(name, age, email);
}

createUser({ name: "Alice", age: 30 });

// Extract to interface for reuse
interface UserOpts {
  name: string;
  age: number;
  email?: string;
}

function updateUser({ name, age }: UserOpts): void {}

// Array destructuring in params
function swap([a, b]: [number, number]): [number, number] {
  return [b, a];
}
06

Классы и ООП

Класс и конструктор

Классы TypeScript поддерживают свойства параметров — преикс параметров конструктора модификаторами доступа (public/private/protected/readonly) автоматически создаёт и присваивает поля. Это сокращение уменьшает шаблонность. Методы могут иметь аннотации типов возвращаемых значений. Поля по умолчанию public.

typescript
class Person {
  // Parameter properties (shorthand)
  constructor(
    public name: string,    // auto-creates this.name
    private age: number,    // private field
    readonly id: number,    // immutable
  ) {}

  greet(): string {
    return `Hi, I'm ${this.name}`;
  }
}

const p = new Person("Alice", 30, 1);
console.log(p.name);  // "Alice" (public)
// p.age; // Error: private
// p.id = 2; // Error: readonly

Модификаторы доступа

Модификаторы доступа: public (по умолчанию, везде), private (только класс), protected (класс + подклассы), readonly (неизменяемый). 'private' в TypeScript существует только на этапе компиляции; приватные поля ES '#' действительно приватны во время выполнения. Используйте private для деталей реализации, protected — для точек расширения.

typescript
class BankAccount {
  public owner: string;       // accessible everywhere
  private balance: number;    // class only
  protected rate: number;     // class + subclasses
  readonly id: string;        // immutable after init
  #secret: string;            // ES private (runtime)

  constructor(owner: string) {
    this.owner = owner;
    this.balance = 0;
    this.rate = 0.05;
    this.id = crypto.randomUUID();
    this.#secret = "hidden";
  }

  deposit(amount: number): void {
    this.balance += amount;
  }
}

Наследование и абстрактные классы

Абстрактные классы нельзя создать напрямую — они определяют базу для подклассов. Абстрактные методы не имеют реализации в базовом классе; подклассы должны их реализовать. Используйте 'extends' для наследования и 'super()' для вызова конструктора родителя. Абстрактные классы обеспечивают полиморфизм — код может работать с любым подклассом Shape.

typescript
abstract class Shape {
  constructor(public color: string) {}
  abstract area(): number;  // must be implemented
  describe(): string {
    return `${this.color} shape, area ${this.area()}`;
  }
}

class Circle extends Shape {
  constructor(color: string, private r: number) {
    super(color);
  }
  area(): number { return Math.PI * this.r ** 2; }
}

class Square extends Shape {
  constructor(color: string, private side: number) {
    super(color);
  }
  area(): number { return this.side ** 2; }
}

const c = new Circle("red", 5);
console.log(c.describe()); // "red shape, area 78.54..."
// new Shape("blue"); // Error: cannot instantiate abstract

Интерфейсы и implements

Класс может реализовать несколько интерфейсов (через запятую). Класс должен предоставить все члены интерфейса. В отличие от extends (одиночное наследование), implements поддерживает несколько контрактов. Это способ TypeScript достичь множественного наследования. Используйте интерфейсы для определения контрактов, классы — для их реализации.

typescript
interface Printable {
  toString(): string;
}

interface Comparable<T> {
  compareTo(other: T): number;
}

class Money implements Printable, Comparable<Money> {
  constructor(private amount: number) {}

  toString(): string {
    return `$${this.amount.toFixed(2)}`;
  }

  compareTo(other: Money): number {
    return this.amount - other.amount;
  }
}

const a = new Money(10);
const b = new Money(20);
console.log(a.toString());      // "$10.00"
console.log(a.compareTo(b));    // -10

Геттеры и сеттеры

Геттеры и сеттеры перехватывают доступ к свойствам для валидации, вычислений или побочных эффектов. Используйте приватное резервное поле (конвенция: префикс подчёркивания). Геттеры обеспечивают вычисляемые свойства (например, fahrenheit из celsius). Сеттеры обеспечивают валидацию. Обращайтесь к ним как к обычным свойствам — без скобок.

typescript
class Temperature {
  private _celsius: number = 0;

  get celsius(): number {
    return this._celsius;
  }

  set celsius(value: number) {
    if (value < -273.15) throw new Error("Below absolute zero");
    this._celsius = value;
  }

  get fahrenheit(): number {
    return this._celsius * 9 / 5 + 32;
  }

  set fahrenheit(value: number) {
    this.celsius = (value - 32) * 5 / 9;
  }
}

const temp = new Temperature();
temp.celsius = 25;
console.log(temp.fahrenheit); // 77
// temp.celsius = -300; // throws

Статические члены и Singleton

Статические члены принадлежат классу, а не экземплярам — доступны через ClassName.member. Используйте static для констант, utility-функций и фабричных методов. Приватный конструктор + статический getInstance() реализуют паттерн Singleton. 'as const' делает статические массивы readonly с литеральными типами.

typescript
class Logger {
  static instance: Logger;
  private logs: string[] = [];

  private constructor() {} // prevent direct new

  static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  static readonly LEVELS = ["INFO", "WARN", "ERROR"] as const;

  log(msg: string): void {
    this.logs.push(msg);
  }
}

const logger = Logger.getInstance();
console.log(Logger.LEVELS); // ["INFO", "WARN", "ERROR"]
// new Logger(); // Error: private constructor
07

Дженерики

Дженерик-функции

Дженерики (<T>) позволяют писать функции, работающие с любым типом, сохраняя типобезопасность. Параметр типа T — это заполнитель, заполняемый при вызове — либо явно (identity<number>), либо выводится из аргументов. Дженерики обеспечивают переиспользуемые, типобезопасные структуры данных и алгоритмы.

typescript
// Generic function: preserves type relationship
function identity<T>(value: T): T {
  return value;
}

const n = identity<number>(42);    // T = number, returns number
const s = identity("hi");          // T inferred as string

// Generic with multiple type params
function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

const p = pair("Alice", 30); // [string, number]

// Generic with array
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

Дженерик-классы

Дженерик-классы (<T>) создают типобезопасные контейнеры. Каждый экземпляр фиксирует конкретный тип — Stack<number> принимает только числа. Это отлавливает ошибки типов при компиляции без накладных расходов во время выполнения (дженерики стираются). Распространено в коллекциях (Stack, Queue, Map) и реактивных обёртках (Observable<T>).

typescript
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get size(): number {
    return this.items.length;
  }
}

const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
console.log(numStack.pop()); // 2

const strStack = new Stack<string>();
strStack.push("hello");

Ограничения дженериков

Ограничения (T extends SomeType) ограничивают типы, которые дженерик может принять. 'T extends HasLength' гарантирует, что у T есть свойство 'length'. 'K extends keyof T' (ограничение keyof) гарантирует существование ключа на объекте, возвращая корректный тип значения. Ограничения обеспечивают типобезопасный доступ к свойствам и вызовы методов на дженериках.

typescript
// Constraint: T must have a 'length' property
interface HasLength {
  length: number;
}

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

logLength("hello");     // 5 (string has length)
logLength([1, 2, 3]);   // 3 (array has length)
// logLength(42);        // Error: number has no length

// Constraint with keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // string
// getProperty(user, "email"); // Error: not a key

Параметры типа по умолчанию

Параметры типа по умолчанию предоставляют запасной тип, когда не указан. Полезно для API с общим случаем (например, ApiResponse по умолчанию string). Значения по умолчанию могут зависеть от более ранних параметров. Комбинируйте с ограничениями (T extends X = DefaultType) для типобезопасных опциональных дженериков.

typescript
// Default type parameter
interface ApiResponse<T = string> {
  data: T;
  status: number;
}

const r1: ApiResponse = { data: "hello", status: 200 };       // T = string
const r2: ApiResponse<number> = { data: 42, status: 200 };    // T = number

// Multiple defaults
class Container<T = string, U = number> {
  constructor(public a: T, public b: U) {}
}

const c1 = new Container("hi", 42);      // Container<string, number>
const c2 = new Container<boolean>(true); // Container<boolean, number>

// Default with constraint
interface Box<T extends object = { id: number }> {
  value: T;
}

Дженерик-интерфейсы и типы

Дженерик-интерфейсы и type aliases создают переиспользуемые, типобезопасные контракты. Repository<T> абстрагирует доступ к данным с согласованным API. Result<T, E> — discriminated union для обработки ошибок без исключений. Параметры типа по умолчанию (E = Error) уменьшают шаблонность для распространённых случаев.

typescript
// Generic interface
interface Repository<T> {
  findById(id: string): Promise<T>;
  save(item: T): Promise<void>;
  delete(id: string): Promise<void>;
}

// Implement with concrete type
class UserRepo implements Repository<User> {
  async findById(id: string): Promise<User> { /* ... */ }
  async save(user: User): Promise<void> { /* ... */ }
  async delete(id: string): Promise<void> { /* ... */ }
}

// Generic type alias with conditional
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

const success: Result<number> = { ok: true, value: 42 };
const failure: Result<string> = { ok: false, error: "not found" };

Условные типы

Условные типы (T extends U ? X : Y) — это if-инструкции на уровне типов. 'infer R' извлекает тип из другого типа (например, возвращаемый тип функции). Условные типы распределяются по объединениям — ToArray<string | number> становится string[] | number[]. Встроенные utilities вроде Exclude, Extract и NonNullable используют это.

typescript
// Conditional type: if T extends U, use X, else Y
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">;   // true
type B = IsString<42>;     // false

// Extract return type
type AsyncReturnType<T> =
  T extends (...args: any[]) => Promise<infer R> ? R : never;

async function fetchUser(): Promise<User> { /* ... */ }
type U = AsyncReturnType<typeof fetchUser>; // User

// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]

// Exclude / Extract built on conditionals
type NonNullable<T> = T extends null | undefined ? never : T;
08

Продвинутые типы

Utility типы

TypeScript предоставляет встроенные utility типы для распространённых преобразований: Partial (все опциональны), Pick (выбрать ключи), Omit (исключить ключи), Record (map ключ-значение), Required (удалить optional), ReturnType (возвращаемый тип функции), Parameters (параметры функции как кортеж). Они устраняют повторяющиеся определения типов.

typescript
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// Partial: all optional (for updates)
type UserUpdate = Partial<User>;
const patch: UserUpdate = { name: "Bob" };

// Pick: select specific keys
type UserSummary = Pick<User, "id" | "name">;

// Omit: exclude specific keys
type CreateUser = Omit<User, "id">;

// Record: key-value map
type UserMap = Record<string, User>;

// Required: all required (remove ?)
type StrictUser = Required<Partial<User>>;

// ReturnType: function return type
type R = ReturnType<() => string>; // string

// Parameters: function parameter types as tuple
type P = Parameters<(a: number, b: string) => void>; // [number, string]

Template Literal типы

Template literal типы (TS 4.1+) строят строковые типы путём интерполяции других типов. В сочетании с объединениями они генерируют декартовы произведения строк. Capitalize/Uppercase преобразуют регистр. Используйте их для типобезопасных имён событий, генерации getter/setter и типизации API-маршрутов. Предложение 'as' в mapped типах обеспечивает переименование ключей.

typescript
// Build string types from other types
type Vertical = "top" | "bottom";
type Horizontal = "left" | "right";
type Position = `${Vertical}-${Horizontal}`;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"

// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Cap = Capitalize<"foo">;    // "Foo"

// Getter names from keys
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; }

// Event listener types
type EventName = `on${Capitalize<"click" | "hover">}`;
// "onClick" | "onHover"

Ключевое слово infer

Ключевое слово 'infer' объявляет переменную типа внутри предложения 'extends' условного типа, захватывая тип для переиспользования. Это основа utility типов вроде ReturnType, Parameters и Awaited. Используйте infer для извлечения типов из сложных структур (массивы, промисы, функции) без ручного разложения.

typescript
// Extract return type of a function
type MyReturnType<T> =
  T extends (...args: any[]) => infer R ? R : never;

type R1 = MyReturnType<() => string>;    // string
type R2 = MyReturnType<(x: number) => boolean>; // boolean

// Extract element type of an array
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E = ElementOf<string[]>;  // string

// Extract Promise value
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type V = Unwrap<Promise<number>>; // number

// Extract first parameter
type FirstParam<T> =
  T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FP = FirstParam<(name: string, age: number) => void>; // string

Discriminated Unions

Discriminated unions (tagged unions) используют общее литеральное поле ('type', 'kind', 'tag') для различения вариантов. TypeScript сужает тип в каждом case switch, предоставляя доступ к специфичным для случая полям. 'never' по умолчанию обеспечивает исчерпывающую проверку — если вы добавите новый случай, компилятор выдаст ошибку, пока вы его не обработаете. Существенно для Redux reducers и конечных автоматов.

typescript
// Discriminated union: shared 'type' (or 'kind') field
type Action =
  | { type: "ADD_TODO"; text: string }
  | { type: "DELETE_TODO"; id: number }
  | { type: "TOGGLE_TODO"; id: number };

function reducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case "ADD_TODO":
      return [...state, { id: Date.now(), text: action.text }];
    case "DELETE_TODO":
      return state.filter(t => t.id !== action.id);
    case "TOGGLE_TODO":
      return state.map(t =>
        t.id === action.id ? { ...t, done: !t.done } : t
      );
    default:
      const _: never = action; // exhaustiveness check
      return state;
  }
}

Type Guards: typeof и instanceof

Type guards сужают типы во время выполнения. 'typeof' работает для примитивов (string, number, boolean, symbol, bigint, undefined, function, object). 'instanceof' проверяет прототипы классов/конструкторов. Array.isArray() сужает до типизированного массива. Это встроенные guards — без пользовательского кода. TypeScript отслеживает суженный тип в каждой ветке.

typescript
// typeof: narrow primitives
function process(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase(); // string
  }
  return value.toFixed(2);      // number
}

// instanceof: narrow class instances
class Dog { bark() {} }
class Cat { meow() {} }

function speak(pet: Dog | Cat) {
  if (pet instanceof Dog) {
    pet.bark(); // Dog
  } else {
    pet.meow(); // Cat
  }
}

// Array.isArray
function flatten(arr: (number | number[])[]) {
  return arr.flatMap(x =>
    Array.isArray(x) ? x : [x]
  );
}

Пользовательские Type Guards и оператор in

Оператор 'in' проверяет существование свойства на объекте, сужая до типа, который его имеет. Type predicates (x is T) — пользовательские guard-функции, возвращающие boolean, но также сужающие тип. Используйте 'unknown' как входной тип для безопасного парсинга внешних данных (JSON.parse, API-ответы). Predicates обеспечивают переиспользуемые, компонуемые проверки типов.

typescript
// 'in' operator: check for property existence
interface Fish { swim(): void; }
interface Bird { fly(): void; }

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim(); // Fish
  } else {
    animal.fly(); // Bird
  }
}

// Type predicate: custom guard function
function isString(x: unknown): x is string {
  return typeof x === "string";
}

function isUser(x: any): x is { name: string; age: number } {
  return x && typeof x.name === "string" && typeof x.age === "number";
}

const val: unknown = "hello";
if (isString(val)) {
  console.log(val.toUpperCase()); // narrowed to string
}
09

Структуры данных

Массивы и ReadonlyArray

Массивы TypeScript типизированы — методы вроде map, filter и reduce сохраняют типы элементов. Используйте readonly T[] или ReadonlyArray<T> для неизменяемости. Кортежи имеют фиксированную длину и типизированные позиции. Деструктуризация массивов и spread полностью типобезопасны. Система типов отлавливает выход за границы индекса и присваивание неправильных типов.

typescript
// Typed arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b", "c"];

// ReadonlyArray (immutable)
const frozen: readonly number[] = [1, 2, 3];
// frozen.push(4); // Error: readonly

// Tuple (fixed length)
let pair: [string, number] = ["Alice", 30];

// Array methods are typed
const doubled = nums.map(n => n * 2);    // number[]
const evens = nums.filter(n => n % 2 === 0); // number[]
const sum = nums.reduce((a, b) => a + b, 0); // number

// Spread and destructuring
const [first, ...rest] = nums;
const combined = [...nums, ...[4, 5]];

Объекты и Records

Record<K, V> создаёт типизированный map с ключами типа K и значениями типа V. Partial<T> делает все свойства опциональными — идеально для операций update/patch. Object.entries/keys/values возвращают типизированные массивы. Используйте Pick и Omit для создания сфокусированных типов из существующих, сохраняя типы DRY.

typescript
// Object type
const user: { name: string; age: number } = { name: "Alice", age: 30 };

// Record: typed key-value map
const scores: Record<string, number> = {
  math: 90,
  science: 85,
};

// Partial: all optional (for patches)
const patch: Partial<typeof user> = { age: 31 };

// Pick / Omit
type Summary = Pick<typeof user, "name">;
type WithoutAge = Omit<typeof user, "age">;

// Object.entries / keys / values typed
const entries = Object.entries(scores); // [string, number][]
const keys = Object.keys(scores);       // string[]
const values = Object.values(scores);   // number[]

Map и Set

Map и Set — коллекции ES6 с полной поддержкой TypeScript. Ключи Map могут быть любого типа (в отличие от объектов, которые приводят ключи к строкам). Set хранит уникальные значения. WeakMap/WeakSet позволяют сборку мусора для ключей — полезно для метаданных, прикреплённых к DOM-элементам или объектам без предотвращения GC.

typescript
// Map: keyed collection (any key type)
const map = new Map<string, User>();
map.set("alice", { name: "Alice", age: 30 });
const user = map.get("alice"); // User | undefined

// Set: unique values
const unique = new Set<number>([1, 2, 2, 3]);
console.log(unique.size); // 3
console.log(unique.has(2)); // true

// Iteration (typed)
for (const [key, value] of map) {
  console.log(key, value.name);
}

for (const num of unique) {
  console.log(num);
}

// WeakMap / WeakSet (keys must be objects, GC-friendly)
const weak = new WeakMap<object, string>();

Кортежи и именованные кортежи

Кортежи — массивы фиксированной длины с типизированными позициями. Именованные кортежи (TS 4.0+) добавляют имена для читаемости — полезно для возвращаемых значений и CSV-подобных данных. Кортежи позволяют множественные возвращаемые значения без создания интерфейса. Используйте 'readonly' для предотвращения мутации. Кортежи отличаются от массивов: [string, number] это НЕ (string | number)[].

typescript
// Basic tuple
let point: [number, number] = [10, 20];

// Labeled tuple (TS 4.0+)
let user: [id: number, name: string, active: boolean] = [1, "Alice", true];

// Destructuring with labels
const [id, name, active] = user;

// Tuple in function returns
function divmod(a: number, b: number): [quotient: number, remainder: number] {
  return [Math.floor(a / b), a % b];
}

const [q, r] = divmod(17, 5);
console.log(q, r); // 3 2

// Readonly tuple
const fixed: readonly [string, number] = ["a", 1];
// fixed.push(2); // Error

Enum'ы и Const Enums

Enum'ы создают именованные константы. Строковые enum'ы рекомендуются (читаемы в выводе, нет проблем с обратным отображением). Const enums удаляются при компиляции (без накладных расходов во время выполнения). Для простых случаев типы объединения ('a' | 'b') часто лучше — нет кода во время выполнения, лучшее tree-shaking. Используйте enum'ы для сгруппированных, документированных констант.

typescript
// Numeric enum
enum Direction { North = 0, South = 1, East = 2, West = 3 }

// String enum (recommended)
enum HttpStatus {
  OK = "200 OK",
  NotFound = "404 Not Found",
  ServerError = "500 Internal Server Error",
}

// Const enum (inlined at compile time)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;

// Union type alternative (no runtime code)
type Status = "pending" | "success" | "error";
const s: Status = "pending";

// Exhaustive switch
function handle(s: Status) {
  switch (s) {
    case "pending": return "loading";
    case "success": return "done";
    case "error": return "failed";
  }
}

Неизменяемые данные

TypeScript предлагает несколько инструментов неизменяемости: 'readonly' для свойств, ReadonlyArray для массивов, utility Readonly<T> и 'as const' для глубокого readonly с литеральными типами. Неизменяемые данные предотвращают случайные мутации и обеспечивают обнаружение изменений (React, Redux). Используйте spread (...) для неизменяемых обновлений — создаёт новый объект с изменёнными полями.

typescript
// Readonly modifier
interface User {
  readonly id: number;
  name: string;
}

// ReadonlyArray
const nums: ReadonlyArray<number> = [1, 2, 3];
// nums[0] = 9; // Error

// Readonly utility type
type FrozenUser = Readonly<User>;

// as const: deep readonly with literal types
const config = {
  endpoint: "/api",
  methods: ["GET", "POST"],
} as const;
// config.endpoint: "/api" (literal, not string)
// config.methods: readonly ["GET", "POST"]

// Immutable update pattern
const user = { name: "Alice", age: 30 };
const updated = { ...user, age: 31 }; // new object
10

Модули и пространства имён

ES Modules: Import и Export

TypeScript использует синтаксис ES модулей (import/export). Именованные экспорты явные; default export — единственный 'основной' экспорт. Используйте 'import * as' для namespace импортов. Разрешение модулей следует конвенциям Node.js (node_modules, расширения). Настраивайте 'module' и 'moduleResolution' в tsconfig.json.

typescript
// math.ts - exporting
export function add(a: number, b: number): number {
  return a + b;
}

export const PI = 3.14159;

export default function multiply(a: number, b: number): number {
  return a * b;
}

// main.ts - importing
import multiply, { add, PI } from "./math";
import * as math from "./math"; // namespace import

console.log(add(1, 2));      // 3
console.log(multiply(3, 4)); // 12 (default)
console.log(math.PI);        // 3.14159

Импорты только типов

'import type' импортирует только типы (стираются при компиляции, нет кода во время выполнения). Это избегает циклических зависимостей и ненужных импортов во время выполнения. TS 4.5+ позволяет инлайн-модификаторы 'type' в смешанных импортах. Используйте импорты только типов для интерфейсов, type aliases и enum'ов (если const) для уменьшения размера bundle.

typescript
// Type-only import (erased at runtime)
import type { User, Config } from "./types";

// Mixed import (TS 4.5+)
import { render, type Component } from "./ui";

// Re-export types
export type { User } from "./types";

// Import type for interfaces and types
interface UserService {
  get(id: string): User;  // User from type-only import
}

// 'import type' ensures no runtime dependency
// Useful when the module has side effects you want to avoid

Динамические импорты

Динамические импорты (import()) загружают модули по требованию, возвращая Promise. Это обеспечивает code splitting и lazy loading — критично для производительности в веб-приложениях. TypeScript автоматически выводит тип модуля. Используйте для опциональных функций, больших библиотек и route-based code splitting (React.lazy, Next.js dynamic).

typescript
// Dynamic import returns a Promise
async function loadModule() {
  const module = await import("./heavy-module");
  module.doWork();
}

// Conditional loading
if (featureEnabled) {
  const { Feature } = await import("./feature");
  new Feature().init();
}

// Type the dynamic import
type HeavyModule = typeof import("./heavy-module");

// With error handling
try {
  const lib = await import("./optional-lib");
  lib.run();
} catch (e) {
  console.log("Library not available");
}

Файлы объявлений и расширение модулей

Файлы объявлений (.d.ts) описывают типы для JS-модулей, импортов CSS/PNG и глобальных переменных. Расширение модулей расширяет существующие типы модулей — полезно для добавления свойств в Express Request, Express Response или сторонние типы. Так middleware вроде passport добавляет типизацию req.user.

typescript
// global.d.ts - declare ambient modules
declare module "*.css" {
  const content: string;
  export default content;
}

declare module "*.png" {
  const src: string;
  export default src;
}

// Module augmentation - extend existing types
import express from "express";

declare module "express" {
  interface Request {
    user?: { id: string; name: string };
  }
}

// Now req.user is typed
app.get("/", (req, res) => {
  console.log(req.user?.name);
});

Пространства имён (устаревшее)

Пространства имён — это система модулей TypeScript до ES6. Они группируют связанный код под именованным объектом. Для новых проектов предпочитайте ES модули (import/export) — они стандартизированы, поддерживают tree-shaking и работают с бандлерами. Пространства имён остаются полезными в .d.ts файлах для глобальных объявлений типов и устаревшего кода.

typescript
// Namespace: pre-ES6 way to organize code
namespace Validation {
  export interface StringValidator {
    isValid(s: string): boolean;
  }

  const lettersRegexp = /^[A-Za-z]+$/;

  export class LettersOnlyValidator implements StringValidator {
    isValid(s: string): boolean {
      return lettersRegexp.test(s);
    }
  }
}

// Usage
const validator = new Validation.LettersOnlyValidator();
console.log(validator.isValid("Hello")); // true

// Prefer ES modules over namespaces for new code
// Namespaces are useful in .d.ts files for global declarations

Настройки модулей tsconfig

Настройки модулей tsconfig управляют тем, как TypeScript обрабатывает импорты. 'moduleResolution: node' использует разрешение Node.js (lookup node_modules). 'esModuleInterop' включает импорт по умолчанию из CommonJS. 'paths' создаёт алиасы импортов (@/components) для более чистых импортов. 'resolveJsonModule' позволяет импортировать .json файлы с выведенными типами.

typescript
{
  "compilerOptions": {
    "module": "ESNext",           // ES module output
    "moduleResolution": "node",   // Node-style resolution
    "esModuleInterop": true,      // allow default imports from CJS
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,    // import .json files
    "isolatedModules": true,      // each file is independent
    "baseUrl": "./src",           // base for non-relative imports
    "paths": {
      "@/*": ["./*"],             // path alias
      "@components/*": ["./components/*"]
    }
  }
}

// With paths config, you can import:
// import { Button } from "@/components/Button";
// instead of relative paths like "../../components/Button"
11

Async и Promises

Типы Promise

Promise<T> — основной async-тип — T это тип разрешённого значения. TypeScript выводит типы через цепочки .then(). Используйте 'new Promise()' для обёртки API на основе обратных вызовов. Всегда типизируйте значения resolve/reject. Предпочитайте async/await вместо сырых цепочек .then() для читаемости и обработки ошибок.

typescript
// Promise<T> represents an async value
function fetchUser(id: number): Promise<User> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id === 1) {
        resolve({ id: 1, name: "Alice" });
      } else {
        reject(new Error("User not found"));
      }
    }, 100);
  });
}

// Consuming a Promise
fetchUser(1)
  .then(user => console.log(user.name))
  .catch(err => console.error(err))
  .finally(() => console.log("done"));

// Promise typing: then callbacks are typed
fetchUser(1).then(user => {
  // user is User, not any
  console.log(user.name); // string
});

async/await

async/await — синтаксический сахар над Promises — 'await' приостанавливается до разрешения Promise. async-функции всегда возвращают Promise. Используйте Promise.all() для параллельного выполнения (гораздо быстрее последовательных await). Top-level await работает в ES модулях с ES2022+. TypeScript проверяет, что awaited значения являются Promises.

typescript
// async function returns Promise<T>
async function getUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json() as Promise<User>;
}

// Top-level await (ES2022, TS 4.7+)
// const user = await getUser(1);

// Sequential vs parallel
async function loadAll() {
  // Sequential (slow)
  const a = await getUser(1);
  const b = await getUser(2);

  // Parallel (fast)
  const [x, y] = await Promise.all([getUser(1), getUser(2)]);
  return { a, b, x, y };
}

Обработка ошибок в Async

Используйте try/catch с async/await для обработки ошибок — это чище, чем .catch(). TypeScript не поддерживает типизированные throw (все ошибки unknown в catch), поэтому сужайте через instanceof. Для предсказуемых ошибок рассмотрите паттерн Result типа (объединение ok/error) вместо исключений — он делает обработку ошибок явной в сигнатуре типа.

typescript
// Try/catch with async/await
async function riskyOperation(): Promise<string> {
  try {
    const data = await fetch("/api/data");
    if (!data.ok) throw new Error(`HTTP ${data.status}`);
    return await data.text();
  } catch (error) {
    if (error instanceof Error) {
      console.error(error.message);
    }
    return "fallback";
  } finally {
    console.log("cleanup");
  }
}

// Typed errors (TypeScript doesn't have typed throws)
class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
  }
}

// Result type as alternative to exceptions
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

async function safeFetch(url: string): Promise<Result<string>> {
  try {
    const res = await fetch(url);
    return { ok: true, value: await res.text() };
  } catch (e) {
    return { ok: false, error: String(e) };
  }
}

Комбинаторы Promise

Комбинаторы Promise оркестрируют несколько async-операций: all() (параллельно, fail-fast), allSettled() (параллельно, ждать все), race() (первый разрешённый), any() (первый успешный). Используйте all() для зависимой загрузки данных, allSettled() когда нужны частичные результаты, race() для таймаутов, any() для избыточных запросов.

typescript
// Promise.all: wait for all (rejects if any rejects)
const [users, posts] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
]);

// Promise.allSettled: wait for all (never rejects)
const results = await Promise.allSettled([
  fetch("/api/a"),
  fetch("/api/b"),
]);
results.forEach(r => {
  if (r.status === "fulfilled") console.log(r.value);
  else console.log(r.reason);
});

// Promise.race: first to settle (resolve or reject)
const fastest = await Promise.race([
  fetch("/api/fast"),
  fetch("/api/slow"),
]);

// Promise.any: first to resolve (ignores rejections)
const first = await Promise.any([
  fetch("/api/primary"),
  fetch("/api/fallback"),
]);

Event Loop и микрозадачи

Event loop JavaScript обрабатывает микрозадачи (обратные вызовы Promise, queueMicrotask) перед макрозадачами (setTimeout, setInterval). Поэтому Promises разрешаются раньше таймаутов. Async-итерация (for await...of) потребляет async-итерируемые объекты — полезно для потоков. Async-генераторы (async function*) создают async-итерируемые объекты, обеспечивая ленивые async-последовательности.

typescript
// Microtasks (Promises) run before macrotasks (setTimeout)
console.log("1: sync");

setTimeout(() => console.log("4: macrotask"), 0);

Promise.resolve().then(() => console.log("3: microtask"));

console.log("2: sync");
// Output: 1, 2, 3, 4

// queueMicrotask for custom microtasks
queueMicrotask(() => console.log("microtask"));

// Async iteration (for await)
async function processStream(stream: AsyncIterable<Buffer>) {
  for await (const chunk of stream) {
    console.log(chunk.length);
  }
}

// Async generators
async function* generate() {
  for (let i = 0; i < 3; i++) {
    await new Promise(r => setTimeout(r, 100));
    yield i;
  }
}

Паттерны конкурентности

Эти паттерны полностью типобезопасны в TypeScript. Debounce откладывает выполнение до прекращения вызовов на N мс (ввод поиска). Throttle ограничивает одним вызовом на N мс (обработчики скролла). Semaphore/mapLimit управляет конкурентностью — полезно для API с ограничением частоты. Parameters<T> и ReturnType<T> сохраняют сигнатуры функций в обёртках.

typescript
// Debounce with TypeScript
function debounce<T extends (...args: any[]) => void>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timer: ReturnType<typeof setTimeout>;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

// Throttle
function throttle<T extends (...args: any[]) => void>(
  fn: T,
  limit: number
): (...args: Parameters<T>) => void {
  let inThrottle = false;
  return (...args) => {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

// Semaphore (limit concurrency)
async function mapLimit<T, U>(
  items: T[],
  limit: number,
  fn: (item: T) => Promise<U>
): Promise<U[]> {
  const results: U[] = [];
  const executing: Promise<void>[] = [];
  for (const item of items) {
    const p = fn(item).then(r => results.push(r));
    executing.push(p);
    if (executing.length >= limit) {
      await Promise.race(executing);
      executing.splice(executing.findIndex(e => e === p), 1);
    }
  }
  await Promise.all(executing);
  return results;
}
12

Обработка ошибок и тестирование

Try/Catch с Unknown

Начиная с TypeScript 4.4 (useUnknownInCatchVariables), пойманные ошибки — 'unknown' — вы должны сузить их перед использованием. Это предотвращает доступ к несуществующим свойствам. Используйте instanceof для проверки конкретных типов ошибок или String() как запасной вариант. Создайте helper getErrorMessage() для согласованного извлечения ошибок.

typescript
// In strict mode, catch is 'unknown' (not 'any')
try {
  JSON.parse("invalid");
} catch (error: unknown) {
  // Must narrow before using
  if (error instanceof SyntaxError) {
    console.error("JSON error:", error.message);
  } else if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error("Unknown error:", error);
  }
}

// Helper function
function getErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  return String(error);
}

Пользовательские классы ошибок

Пользовательские классы ошибок добавляют структурированные данные (code, statusCode, field) к ошибкам. Всегда вызывайте super(message) и устанавливайте prototype (Object.setPrototypeOf) для исправления проблемы цепочки прототипов TypeScript/ES5. Используйте instanceof для различения типов ошибок в блоках catch. Этот паттерн существенен для middleware Express и обработчиков ошибок API.

typescript
class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number = 500
  ) {
    super(message);
    this.name = "AppError";
    // Fix prototype chain (TS quirk)
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

class ValidationError extends AppError {
  constructor(message: string, public field: string) {
    super(message, "VALIDATION_ERROR", 400);
    this.name = "ValidationError";
  }
}

// Usage
function createUser(input: unknown) {
  if (typeof input !== "object" || input === null) {
    throw new ValidationError("Invalid input", "body");
  }
}

try {
  createUser("bad");
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(e.field, e.statusCode); // "body" 400
  }
}

Паттерн Result типа

Тип Result (из Rust) делает ошибки явными в сигнатуре типа — вызывающие должны обработать и успех, и неудачу. В отличие от исключений, компилятор обеспечивает обработку ошибок. Используйте для ожидаемых неудач (валидация, not-found), где исключения были бы избыточны. Оставьте исключения для действительно неожиданных ошибок (баги, системные сбои).

typescript
// Result type: explicit error handling without exceptions
type Result<T, E = string> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function divide(a: number, b: number): Result<number> {
  if (b === 0) {
    return { ok: false, error: "Division by zero" };
  }
  return { ok: true, value: a / b };
}

// Usage: forced to handle both cases
const result = divide(10, 0);
if (result.ok) {
  console.log(result.value); // number
} else {
  console.error(result.error); // string
}

// Utility helpers
function ok<T>(value: T): Result<T, never> {
  return { ok: true, value };
}

function err<E>(error: E): Result<never, E> {
  return { ok: false, error };
}

Assertion-функции

Assertion-функции (asserts X) выбрасывают исключение, если условие не выполнено, И сужают тип после. 'asserts value is string' сообщает TypeScript, что после вызова value это string. Это чище, чем повторяющиеся if-проверки. Используйте для runtime-валидации на границах (ввод API, конфигурация). Комбинируйте с Zod или io-ts для валидации схем.

typescript
// Assertion function: throws if condition is false
function assert(condition: unknown, message: string): asserts condition {
  if (!condition) {
    throw new Error(message);
  }
}

// Assert value is a specific type
function assertString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error(`Expected string, got ${typeof value}`);
  }
}

// Usage: narrows type after assertion
const input: unknown = "hello";
assertString(input);
console.log(input.toUpperCase()); // input is now string

// Assert non-null
function assertDefined<T>(value: T | undefined): asserts value is T {
  if (value === undefined) throw new Error("undefined");
}

Типобезопасный парсинг JSON

JSON.parse возвращает 'any' — небезопасно. Оберните его type guard'ом для валидации формы во время выполнения и сужения типа. Для сложных схем используйте Zod, io-ts или yup — они генерируют и runtime-валидаторы, и TypeScript-типы из единого определения схемы. Это критично для API-ответов и пользовательского ввода.

typescript
// Safe JSON parse with type guard
function safeParse<T>(json: string, guard: (x: unknown) => x is T): T | null {
  try {
    const parsed: unknown = JSON.parse(json);
    if (guard(parsed)) return parsed;
    return null;
  } catch {
    return null;
  }
}

// Type guard for User
interface User { id: number; name: string; }
function isUser(x: unknown): x is User {
  return typeof x === "object" && x !== null
    && typeof (x as User).id === "number"
    && typeof (x as User).name === "string";
}

const data = safeParse('{"id":1,"name":"Alice"}', isUser);
if (data) {
  console.log(data.name); // User
}

// Using Zod for runtime validation
// import { z } from "zod";
// const UserSchema = z.object({ id: z.number(), name: z.string() });
// const user = UserSchema.parse(JSON.parse(json));

Исчерпывающая проверка

Исчерпывающая проверка гарантирует, что вы обрабатываете все случаи объединения. Присвойте случаю default 'never' — если вы добавите новый вариант в объединение, TypeScript выдаст ошибку, так как новый тип не присваивается 'never'. Это отлавливает пропущенные случаи при компиляции. Существенно для discriminated unions, Redux reducers и конечных автоматов.

typescript
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number }
  | { kind: "rectangle"; w: number; h: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    case "rectangle":
      return shape.w * shape.h;
    default:
      // If a new shape is added, this errors
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

// Adding a new variant:
// | { kind: "triangle"; base: number; height: number }
// Now the default case errors: Type 'triangle' is not assignable to 'never'
13

Декораторы и метаданные

Декораторы классов

Декораторы классов получают функцию-конструктор и могут вернуть изменённый класс. Фабрики декораторов (возвращающие функцию) принимают аргументы. Декораторы — экспериментальная функция — включите 'experimentalDecorators' в tsconfig. Широко используются в NestJS, TypeORM и Angular для dependency injection и метаданных.

typescript
// Class decorator: receives the constructor
function Logged<T extends new (...args: any[]) => any>(target: T): T {
  return class extends target {
    constructor(...args: any[]) {
      super(...args);
      console.log(`Created ${target.name}`);
    }
  };
}

@Logged
class Service {
  constructor(public name: string) {}
}

const s = new Service("Auth");
// Logs: "Created Service"

// Decorator factory (with arguments)
function Prefix(prefix: string) {
  return function <T extends new (...args: any[]) => any>(target: T): T {
    return class extends target {
      message = prefix + " " + (this as any).name;
    };
  };
}

Декораторы методов и свойств

Декораторы методов получают (target, propertyKey, descriptor) и могут обернуть оригинальный метод — полезно для логирования, кэширования и контроля доступа. Декораторы свойств получают (target, key) и часто используются для регистрации метаданных. descriptor.value — оригинальная функция; оберните её для добавления поведения. Распространено в NestJS (@Get, @Post) и TypeORM (@Column).

typescript
// Method decorator: (target, key, descriptor)
function Log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey}(${args})`);
    return original.apply(this, args);
  };
}

class Calculator {
  @Log
  add(a: number, b: number): number {
    return a + b;
  }
}

new Calculator().add(2, 3);
// Logs: "Calling add(2,3)"

// Property decorator: (target, key)
function Required(target: any, key: string) {
  // Store metadata for validation
  console.log(`Required: ${key}`);
}

Декораторы параметров и метаданные

Декораторы параметров получают (target, key, index) и используются для dependency injection (NestJS, Angular). Полифил 'reflect-metadata' включает метаданные типов во время выполнения — декораторы могут получать доступ к типам параметров через Reflect.getMetadata('design:paramtypes'). Так DI-контейнеры знают, что внедрять. Включите через 'emitDecoratorMetadata: true' в tsconfig.

typescript
import "reflect-metadata";

// Parameter decorator
function Inject(target: any, key: string, index: number) {
  console.log(`Inject param ${index} of ${key}`);
}

class Service {
  constructor(@Inject private dep: any) {}
}

// Store and retrieve metadata
const METADATA_KEY = "design:type";

class Example {
  greet(name: string): void {}
}

// reflect-metadata provides:
// - design:type (property type)
// - design:paramtypes (method parameter types)
// - design:returntype (method return type)

const types = Reflect.getMetadata("design:paramtypes", Example.prototype, "greet");
// types: [String]

Декораторы аксессоров

Декораторы аксессоров применяются к геттерам/сеттерам. Дескриптор имеет свойства get/set, которые можно обернуть. Используйте их для валидации, логирования или изменения перечислимости. Паттерн валидации (MaxLength, Min, Max) оборачивает сеттер для применения ограничений во время выполнения. Так работает class-validator (NestJS) для валидации DTO.

typescript
// Accessor decorator (getter/setter)
function Enumerable(value: boolean) {
  return function (
    target: any,
    key: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.enumerable = value;
  };
}

class Person {
  private _name: string = "";

  @Enumerable(true)
  get name(): string {
    return this._name;
  }

  set name(value: string) {
    this._name = value;
  }
}

const p = new Person();
p.name = "Alice";
console.log(Object.keys(p)); // ["name"] (enumerable)

// Validation decorator
function MaxLength(len: number) {
  return function (target: any, key: string) {
    let value: string;
    Object.defineProperty(target, key, {
      get() { return value; },
      set(v: string) {
        if (v.length > len) throw new Error("Too long");
        value = v;
      },
    });
  };
}

Современные декораторы (TC39 Stage 3)

TypeScript 5.0 поддерживает предложение декораторов TC39 Stage 3 — стандартизированный API, заменяющий experimentalDecorators. Новый API использует контекстный объект (ClassMethodDecoratorContext) вместо (target, key, descriptor). Он чище, типобезопасен и в итоге будет в стандарте JS. Используйте для новых проектов; экспериментальные декораторы остаются для совместимости с NestJS/Angular.

typescript
// TC39 Stage 3 decorators (TS 5.0+, no experimentalDecorators needed)
// Uses a different API: context object

function log<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
  return function (this: This, ...args: Args): Return {
    console.log(`Calling ${String(context.name)}`);
    return target.call(this, ...args);
  };
}

class Service {
  @log
  greet(name: string): string {
    return `Hello, ${name}`;
  }
}

// Class method decorator context provides:
// - name: property name
// - kind: "method" | "field" | "class"
// - access: { get(), set(value) } for fields
// - addInitializer(): run code after construction

Практичный декоратор: Memoize

Этот декоратор memoize кэширует результаты методов на основе аргументов — значительное ускорение для дорогих чистых функций вроде fibonacci. Кэш на экземпляр (используйте WeakMap для общего кэша). Декораторы блестят для сквозных задач: логирование, кэширование, валидация, контроль доступа, логика повторов. Они сохраняют бизнес-логику чистой, разделяя инфраструктурные задачи.

typescript
// Memoize: cache method results
function Memoize<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext
) {
  const cache = new Map<string, Return>();
  return function (this: This, ...args: Args): Return {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key)!;
    const result = target.call(this, ...args);
    cache.set(key, result);
    return result;
  };
}

class MathService {
  @Memoize
  fibonacci(n: number): number {
    if (n < 2) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

const svc = new MathService();
console.time("first");
console.log(svc.fibonacci(40)); // slow
console.timeEnd("first");

console.time("second");
console.log(svc.fibonacci(40)); // fast (cached)
console.timeEnd("second");
14

Utility типы

Partial, Required и Readonly

Partial<T> делает все свойства опциональными — идеально для операций update/patch, где изменяются только некоторые поля. Required<T> — обратное. Readonly<T> делает все свойства неизменяемыми при компиляции. Это самые часто используемые utility типы, устраняющие необходимость в ручном поддержании параллельных optional/readonly интерфейсов.

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

// Partial<T>: all properties become optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string }

function updateUser(id: number, changes: UserUpdate) {
  // Only provided fields are updated
  Object.assign(users[id], changes);
}
updateUser(1, { name: "Alice" }); // OK — only name

// Required<T>: all properties become required (inverse of Partial)
type StrictUser = Required<Partial<User>>; // back to all required

// Readonly<T>: all properties become readonly
type FrozenUser = Readonly<User>;
const frozen: FrozenUser = { id: 1, name: "Alice", email: "[email protected]" };
// frozen.name = "Bob"; // Error: readonly

// Practical: immutable config objects
const config: Readonly<Config> = { /* ... */ };
// config.port = 8081; // Error — prevents accidental mutation

Pick, Omit и Record

Pick<T, K> извлекает подмножество свойств; Omit<T, K> удаляет свойства — оба создают производные типы без дублирования. Record<K, V> создаёт тип map/словаря с конкретными ключами. Существенно для DTO: создайте тип CreateUser из User, удалив автогенерируемые поля вроде id и createdAt. Это сохраняет типы DRY и синхронизированными.

typescript
interface User {
  id: number;
  name: string;
  email: string;
  role: string;
  createdAt: Date;
}

// Pick<T, Keys>: select specific properties
type UserSummary = Pick<User, "id" | "name">;
// { id: number; name: string }

// Omit<T, Keys>: remove specific properties
type UserInput = Omit<User, "id" | "createdAt">;
// { name: string; email: string; role: string }

// Record<Keys, Value>: object with specific keys and value type
type UserRole = "admin" | "user" | "guest";
type Permissions = Record<UserRole, string[]>;
const perms: Permissions = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"],
};

// Combining: create a DTO from a full entity
type UserDTO = Pick<User, "id" | "name" | "email">;
type CreateUserDTO = Omit<User, "id" | "createdAt">;

ReturnType, Parameters и Awaited

ReturnType и Parameters извлекают типы из существующих функций — неоценимо при обёртывании или вызове функций, сигнатуры которых вы не хотите дублировать. Awaited<T> разворачивает вложенные Promises (Promise<Promise<T>> становится T), существенно для возвращаемых типов async-функций. InstanceType получает тип экземпляра из конструктора класса. Это обеспечивает типобезопасную композицию функций и utilities высшего порядка.

typescript
function fetchUser(id: number): Promise<{ name: string; age: number }> {
  return Promise.resolve({ name: "Alice", age: 30 });
}

// ReturnType<T>: the return type of a function
type FetchResult = ReturnType<typeof fetchUser>;
// Promise<{ name: string; age: number }>

// Awaited<T>: unwrap a Promise to its inner type
type User = Awaited<ReturnType<typeof fetchUser>>;
// { name: string; age: number }

// Parameters<T>: tuple of parameter types
type FetchParams = Parameters<typeof fetchUser>;
// [id: number]

// First parameter type
type FirstParam = Parameters<typeof fetchUser>[0]; // number

// ConstructorParameters<T>: parameters of a class constructor
class Point {
  constructor(public x: number, public y: number) {}
}
type PointArgs = ConstructorParameters<typeof Point>; // [x: number, y: number]

// InstanceType<T>: the instance type of a constructor
type PointInstance = InstanceType<typeof Point>; // Point

Exclude, Extract и NonNullable

Exclude<T, U> удаляет типы из объединения; Extract<T, U> оставляет только совпадающие типы — оба работают с членами объединения. NonNullable<T> удаляет null и undefined. Это строительные блоки: Omit определяется как Pick<T, Exclude<keyof T, K>>. Используйте Exclude/Extract для динамической фильтрации типов объединения, например, отделения типов ошибок от типов успеха в result union.

typescript
type Role = "admin" | "user" | "guest" | "superadmin";

// Exclude<T, U>: remove types from a union
type NonAdmin = Exclude<Role, "admin" | "superadmin">;
// "user" | "guest"

// Extract<T, U>: keep only matching types from a union
type AdminRoles = Extract<Role, "admin" | "superadmin">;
// "admin" | "superadmin"

// NonNullable<T>: remove null and undefined
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>; // User

// Practical: filter union types
type EventMap = {
  click: MouseEvent;
  keydown: KeyboardEvent;
  scroll: UIEvent;
};
type EventName = keyof EventMap; // "click" | "keydown" | "scroll"

// Omit<T, K> is actually built from Pick and Exclude:
// type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

Пользовательские utility типы

Пользовательские utility типы компонуют встроенные для конкретных нужд. Optional<T, K> делает опциональными только определённые поля (точнее, чем Partial). DeepPartial/DeepReadonly рекурсивно применяются к вложенным объектам — полезно для конфигурации и деревьев состояния. Модификатор -readonly в Mutable удаляет readonly. Эти паттерны показывают, как mapped типы и условные типы сочетаются для мощного type-level программирования.

typescript
// Make specific properties optional
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface User {
  id: number;
  name: string;
  email: string;
}
type UserWithOptionalEmail = Optional<User, "email">;
// { id: number; name: string; email?: string }

// Make specific properties required
type Require<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

// Deep partial (recursively make all properties optional)
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

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

// Mutable (remove readonly from all properties)
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

// Non-never keys (filter out never-valued properties)
type NonNeverKeys<T> = {
  [K in keyof T]: T[K] extends never ? never : K;
}[keyof T];
15

Условные типы

Базовые условные типы (T extends U ? X : Y)

Условные типы (T extends U ? X : Y) выбирают тип на основе условия уровня типа — как тернар для типов. Они — основа type-level программирования TypeScript. Когда T — объединение, условие распределяется по каждому члену (дистрибутивные условные типы). Ключевое слово infer извлекает типы из паттерна, например, тип элемента из массива или тип resolve из Promise.

typescript
// Conditional types choose a type based on a condition
// Syntax: T extends U ? X : Y

// Is T an array? Return its element type, else never
type ElementOf<T> = T extends (infer E)[] ? E : never;

type A = ElementOf<string[]>;    // string
type B = ElementOf<number[]>;    // number
type C = ElementOf<string>;      // never (not an array)

// Is T a Promise? Unwrap it, else keep T
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type D = UnwrapPromise<Promise<number>>;  // number
type E = UnwrapPromise<string>;           // string (not a promise)

// Check if a type is a function
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;

type F = IsFunction<() => void>;  // true
type G = IsFunction<number>;      // false

// Conditional types are evaluated lazily and distribute over unions

Ключевое слово infer (извлечение типа)

Ключевое слово infer объявляет переменную типа внутри предложения extends условного типа, захватывая любой тип, соответствующий этой позиции. Так реализованы ReturnType, Parameters и Awaited. infer может использоваться рекурсивно (Unwrap<Promise<Promise<T>>>) для полного разворачивания вложенных типов. Это основной инструмент для извлечения типов из сложных дженерик-структур.

typescript
// infer extracts a type from within a pattern

// Get the return type of a function (like ReturnType<T>)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Get the first parameter type
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FA = FirstArg<(name: string, age: number) => void>; // string

// Get the resolved value of a Promise (like Awaited)
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
type Deep = Unwrap<Promise<Promise<Promise<number>>>>; // number (recursive!)

// Extract the value type from a Map
type MapValue<M> = M extends Map<any, infer V> ? V : never;
type MV = MapValue<Map<string, number>>; // number

// Extract element type from Set
type SetElement<S> = S extends Set<infer E> ? E : never;

// Get the instance type from a constructor
type Instance<T> = T extends new (...args: any[]) => infer I ? I : never;

Дистрибутивные условные типы

Условные типы распределяются по объединениям: применение ToArray<A | B> даёт ToArray<A> | ToArray<B>, а не (A | B)[]. Так Exclude и NonNullable фильтруют члены объединения — они возвращают 'never' для исключённых типов, который сворачивается в объединении. Чтобы предотвратить распределение, оберните обе стороны в скобки: [T] extends [U]. Распределение обычно нужно для фильтрации, но недистрибутивное необходимо для операций 'обернуть всё объединение'.

typescript
// Conditional types DISTRIBUTE over unions
// T extends U ? X : Y  applied to  A | B  becomes
// (A extends U ? X : Y) | (B extends U ? X : Y)

type ToArray<T> = T extends any ? T[] : never;

type Result = ToArray<string | number>;
// ToArray<string> | ToArray<number>
// = string[] | number[]

// WITHOUT distribution (wrap in brackets to prevent):
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// (string | number)[]  — a single array of the union

// Practical: filter out types from a union
type ExcludeNull<T> = T extends null | undefined ? never : T;
type Cleaned = ExcludeNull<string | null | number | undefined>;
// string | number  (null and undefined filtered out)

// This is exactly how NonNullable<T> works:
// type NonNullable<T> = T extends null | undefined ? never : T;

Ограничения условных типов

Условные типы могут быть вложенными для создания type-level дискриминации (как switch для типов). В сочетании с infer они извлекают и выводят типы из дженерик-параметров. Так библиотеки вроде React выводят типы props из определений компонентов, а библиотеки маршрутизации извлекают типы параметров из строк пути. Ограничение (T extends any[]) гарантирует корректность входных данных перед извлечением типа элемента условным типом.

typescript
// Use conditional types to constrain and derive types

// Get the props type of a React component
type PropsOf<C> = C extends React.ComponentType<infer P> ? P : never;
type ButtonProps = PropsOf<typeof Button>; // the component's prop type

// Conditional with multiple branches (nested)
type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

type T1 = TypeName<string>;     // "string"
type T2 = TypeName<() => void>; // "function"
type T3 = TypeName<{}>;         // "object"

// Constraint + conditional: only allow arrays, get element type
function firstElement<T extends any[]>(arr: T): T extends (infer E)[] ? E : never {
  return arr[0] as any;
}
const x: number = firstElement([1, 2, 3]); // number

Template Literal типы (манипуляция строками)

Template literal типы обеспечивают type-level манипуляцию строками — конкатенацию, преобразование регистра и сопоставление паттернов. В сочетании с условными типами и infer они могут парсить строки пути для извлечения параметров маршрута, генерировать имена обработчиков событий или создавать типобезопасные аксессоры свойств. Так фреймворки вроде Next.js и tRPC создают end-to-end типобезопасные API из строковых литералов.

typescript
// Template literal types: type-level string operations

type Greeting = `Hello ${string}`;
const g: Greeting = "Hello World"; // OK
// const bad: Greeting = "Hi World"; // Error

// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"WORLD">; // "world"
type Cap = Capitalize<"foo">;    // "Foo"

// Build event handler names from event names
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

// Extract route parameters
type ExtractParams<Path extends string> =
  Path extends `${infer _Start}/${infer Param}/${infer Rest}`
    ? Param | ExtractParams<`/${Rest}`>
    : Path extends `${infer _Start}/${infer Param}`
    ? Param
    : never;

type Params = ExtractParams<"/users/:id/posts/:postId">;
// ":id" | ":postId"

// Property accessor type: "a.b.c" -> nested type
type Get<T, P extends string> =
  P extends `${infer Key}.${infer Rest}`
    ? Key extends keyof T ? Get<T[Key], Rest> : never
    : P extends keyof T ? T[P] : never;
16

Mapped типы

Базовые mapped типы

Mapped типы итерируются по ключам объекта и преобразуют каждое свойство — [K in keyof T] это синтаксис. Так реализованы Partial, Readonly, Pick и другие utility типы. Вы можете изменить тип свойства (T[K] | null), добавить модификаторы (? или readonly) или полностью заменить тип значения. Mapped типы — основа системы преобразования типов TypeScript.

typescript
// Mapped types transform each property of an existing type

// Make all properties optional (like Partial<T>)
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Make all properties readonly (like Readonly<T>)
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Make all properties nullable
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

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

type NullableUser = Nullable<User>;
// { id: number | null; name: string | null; email: string | null }

// Change all property types to a specific type
type Stringify<T> = {
  [K in keyof T]: string;
};
type StringUser = Stringify<User>;
// { id: string; name: string; email: string }

Переименование ключей через 'as'

Переименование ключей (предложение as, TS 4.1+) позволяет переименовывать или фильтровать ключи во время маппинга. Используйте template literal типы для преобразования имён ключей (добавление префиксов, преобразование в геттеры, верхний регистр). Возврат 'never' для ключа удаляет его — так вы фильтруете свойства. В сочетании с условными типами переименование ключей обеспечивает мощные преобразования, например, преобразование схемы данных в схему валидации или типа API в тип формы.

typescript
// Remap keys using 'as' (TypeScript 4.1+)

// Add a prefix to all keys
type Prefix<T, P extends string> = {
  [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};

interface User {
  id: number;
  name: string;
}
type PrefixedUser = Prefix<User, "user">;
// { userId: number; userName: string }

// Convert keys to getters
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string }

// Filter out keys by returning never
type RemoveKindField<T> = {
  [K in keyof T as Exclude<K, "kind">]: T[K];
};
type WithoutKind = RemoveKindField<{ kind: "circle"; radius: number }>;
// { radius: number }

// Make keys UPPERCASE
type UpperKeys<T> = {
  [K in keyof T as Uppercase<string & K>]: T[K];
};

Модификаторы: +, -, ?, readonly

Модификаторы + и - добавляют или удаляют модификаторы свойств. -? удаляет опциональность (делая опциональные поля обязательными); -readonly удаляет неизменяемость. Так работают Required<T> и паттерн Mutable. Префикс + опционален (readonly то же, что +readonly), но - требуется для удаления. Это даёт точный контроль над характеристиками свойств во время преобразований типов.

typescript
// Add (+) or remove (-) modifiers

// Remove optional (?) modifier: -?
type Concrete<T> = {
  [K in keyof T]-?: T[K];
};

interface OptionalUser {
  id?: number;
  name?: string;
}
type RequiredUser = Concrete<OptionalUser>;
// { id: number; name: string } — all required now

// Remove readonly modifier: -readonly
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

interface FrozenConfig {
  readonly port: number;
  readonly host: string;
}
type EditableConfig = Mutable<FrozenConfig>;
// { port: number; host: string } — mutable now

// Add readonly modifier: +readonly (or just readonly)
type Freeze<T> = {
  +readonly [K in keyof T]: T[K];
};

// Add optional modifier: +?
type MakeOptional<T> = {
  [K in keyof T]+?: T[K];
};

Гомоморфные mapped типы

Гомоморфные mapped типы ([K in keyof T]) сохраняют модификаторы свойств (readonly, ?) из исходного типа — поэтому Pick<User, 'id'> сохраняет id readonly. Негомоморфные маппинги (например, [K in string]) не сохраняют модификаторы. Это важно при выводе типов: гомоморфный Partial типа с readonly полями сохраняет эти поля readonly (но опциональными). Понимание гомоморфизма помогает предсказывать, выживут ли модификаторы после преобразования.

typescript
// Homomorphic mapped types preserve modifiers from the original type

interface User {
  readonly id: number;
  name: string;
  email?: string;
}

// Homomorphic: [K in keyof T] — preserves readonly and ?
type Clone<T> = {
  [K in keyof T]: T[K];
};
type ClonedUser = Clone<User>;
// { readonly id: number; name: string; email?: string }
// Modifiers are PRESERVED from User

// Non-homomorphic: doesn't preserve modifiers
type NonHomo<T> = {
  [K in string]: T; // not keyof T — loses modifiers
};

// Pick is homomorphic — preserves modifiers
type UserPick = Pick<User, "id" | "name">;
// { readonly id: number; name: string } — readonly preserved!

// Practical: create a "patch" type that preserves optionality
type Patch<T> = {
  [K in keyof T]?: T[K]; // Partial is homomorphic
};

Создание типа валидации из схемы

Так библиотеки форм (React Hook Form, Formik) и библиотеки валидации (Zod, Yup) поддерживают типобезопасность — они выводят типы валидатора и формы из ваших интерфейсов данных с помощью mapped типов. При добавлении поля в User тип валидатора и формы автоматически требуют его, предотвращая рассинхронизацию. Это демонстрирует реальную мощь mapped типов: один источник истины (интерфейс) управляет несколькими производными типами.

typescript
// Practical: derive a validator type from a data type

// Given a data interface
interface User {
  id: number;
  name: string;
  email: string;
}

// Create a validator type where each field is a validation function
type Validator<T> = {
  [K in keyof T]: (value: T[K]) => boolean;
};

const userValidator: Validator<User> = {
  id: (v) => v > 0,
  name: (v) => v.length > 0,
  email: (v) => v.includes("@"),
};

// Create a form type where fields are wrapped in a FormField
type FormField<T> = {
  value: T;
  error: string | null;
  touched: boolean;
};

type Form<T> = {
  [K in keyof T]: FormField<T[K]>;
};

const userForm: Form<User> = {
  id: { value: 1, error: null, touched: false },
  name: { value: "Alice", error: null, touched: true },
  email: { value: "", error: "Required", touched: true },
};

// The form type is always in sync with User — add a field to
// User and the form type automatically requires it too.
17

Type Guards и сужение

Сужение typeof и instanceof

TypeScript сужает типы на основе runtime-проверок. typeof сужает примитивы (string, number, boolean и т.д.); instanceof сужает экземпляры классов. Проверки на истинность (if (value)) сужают, исключая null/undefined/0/''/false. Сужение применяется внутри ветки, где условие выполняется. Так TypeScript делает runtime-проверки носителями типовой информации, устраняя необходимость в явных приведения.

typescript
// typeof narrows primitive types
function padLeft(value: string | number, padding: string | number) {
  if (typeof padding === "number") {
    return " ".repeat(padding) + value;
    // padding is narrowed to 'number' here
  }
  return padding + value;
  // padding is narrowed to 'string' here
}

// instanceof narrows class types
class Cat { meow(): void {} }
class Dog { bark(): void {} }

function speak(animal: Cat | Dog) {
  if (animal instanceof Cat) {
    animal.meow(); // OK — narrowed to Cat
  } else {
    animal.bark(); // OK — narrowed to Dog
  }
}

// typeof returns: "string" | "number" | "boolean" | "symbol"
//   "bigint" | "undefined" | "object" | "function"
// Note: typeof null === "object" (historical JS bug)

// Truthiness narrowing
function process(value?: string) {
  if (value) {
    console.log(value.toUpperCase()); // value is string (not undefined)
  }
}

Оператор in и Discriminated Unions

Оператор 'in' сужает на основе существования свойства. Discriminated unions используют общее литеральное свойство (например, 'kind' или 'type') как тег — switch по нему сужает до правильного варианта с полным доступом к свойствам. Это TypeScript-эквивалент sum types / алгебраических типов данных. Стандартный паттерн для действий Redux, конечных автоматов и API-ответов с несколькими формами. Всегда используйте литеральный тип для дискриминанта.

typescript
// 'in' operator checks for a property — narrows to types that have it

interface Fish { swim(): void; }
interface Bird { fly(): void; }

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim(); // narrowed to Fish
  } else {
    animal.fly(); // narrowed to Bird
  }
}

// Discriminated unions: a shared literal property (the 'discriminant')
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number }
  | { kind: "rectangle"; width: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2; // narrowed by kind
    case "square":
      return shape.size ** 2;
    case "rectangle":
      return shape.width * shape.height;
  }
}

// Discriminated unions are the idiomatic TS pattern for
// variant data (like Redux actions, AST nodes, API responses)

Пользовательские функции Type Guard

Пользовательские type guards (возвращаемый тип 'x is Type') позволяют инкапсулировать сложные runtime-проверки в переиспользуемые функции, сужающие типы. Assertion-функции (asserts x is T) выбрасывают исключение вместо возврата boolean — они сужают во всём коде после вызова. Используйте type guards для валидации ненадёжных данных (JSON.parse, API-ответы) и приведения их в систему типов. Это мост между runtime-валидацией и compile-time типами.

typescript
// A type guard function has a special return type: 'x is Type'
// It narrows the type when it returns true

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

function isUser(obj: any): obj is User {
  return (
    typeof obj === "object" &&
    obj !== null &&
    typeof obj.id === "number" &&
    typeof obj.name === "string"
  );
}

const data: unknown = JSON.parse('{"id": 1, "name": "Alice"}');

if (isUser(data)) {
  console.log(data.name); // data is narrowed to User
}

// Type guard for arrays
function isStringArray(arr: unknown): arr is string[] {
  return Array.isArray(arr) && arr.every((x) => typeof x === "string");
}

// Type guard for discriminated unions
type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string };

function isSuccess<T>(r: Result<T>): r is { success: true; data: T } {
  return r.success;
}

// Assertion functions (TS 3.7+): throw if condition fails
function assertDefined<T>(value: T | null | undefined): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error("Expected value to be defined");
  }
}

Исчерпывающая проверка с never

Исчерпывающая проверка использует тип 'never' для гарантии обработки всех вариантов объединения. Если вы добавите новый вариант в объединение, но забудете случай, присваивание 'never' в ветке default станет ошибкой компиляции. Helper assertNever выбрасывает исключение во время выполнения и выдаёт ошибку при компиляции для пропущенных случаев. Это самый ценный паттерн для discriminated unions — он заставляет компилятор сообщать о забытых случаях.

typescript
// The 'never' type ensures all cases are handled

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    default:
      // If we add a new shape but forget a case, this line
      // becomes a compile error because 'shape' would not be 'never'
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

// Adding a new variant without updating area():
// type Shape = ... | { kind: "triangle"; base: number; height: number }
// Error: Type '{ kind: "triangle"; ... }' is not assignable to type 'never'

// Helper function pattern
function assertNever(x: never): never {
  throw new Error("Unexpected: " + JSON.stringify(x));
}

function process(shape: Shape) {
  switch (shape.kind) {
    case "circle": return;
    case "square": return;
    default: return assertNever(shape); // compile error if a case is missing
  }
}

Сужение с методами массивов

Array.filter не сужает типы элементов по умолчанию, так как его обратный вызов возвращает boolean, а не type guard. Для сужения передайте пользовательскую функцию type guard (pet is Dog) — тогда filter вернёт суженный тип массива. TypeScript также сужает внутри тел обратных вызовов (forEach, map) на основе if-проверок. Array.isArray — встроенный type guard, сужающий unknown/any до типа массива.

typescript
// TypeScript narrows through filter, map, and control flow

interface Pet {
  name: string;
  speak(): void;
}

class Dog implements Pet {
  name = "Rex";
  speak() { console.log("Woof"); }
  fetch() { console.log("Fetching!"); }
}

class Cat implements Pet {
  name = "Whiskers";
  speak() { console.log("Meow"); }
}

const pets: Pet[] = [new Dog(), new Cat(), new Dog()];

// filter doesn't narrow by default (callback return type is boolean)
// Use a type guard to narrow:
function isDog(pet: Pet): pet is Dog {
  return pet instanceof Dog;
}

const dogs = pets.filter(isDog); // Dog[] — properly narrowed!
dogs.forEach((d) => d.fetch()); // OK — d is Dog

// Narrowing in forEach/callbacks
pets.forEach((pet) => {
  if (isDog(pet)) {
    pet.fetch(); // narrowed to Dog inside callback
  }
});

// Array.isArray narrows 'unknown' to an array
function process(input: unknown) {
  if (Array.isArray(input)) {
    input.length; // OK — input is any[] (or unknown[])
  }
}
18

Вывод типов

Вывод переменных и возвращаемых типов

TypeScript выводит типы из инициализаторов и return-инструкций, поэтому явные аннотации редко нужны. Переменные расширяются до общего типа (let x = 10 выводит number, а не 10). 'as const' предотвращает расширение: делает литералы литералами, объекты readonly, а массивы — readonly кортежами. typeof colors[number] извлекает объединение типов элементов кортежа — распространённый паттерн для вывода enum-подобных типов из массивов.

typescript
// TypeScript infers types when you don't annotate

// Variable inference
let count = 0;          // number
let name = "Alice";     // string
let items = [1, 2, 3];  // number[]
let mixed = [1, "two"]; // (string | number)[]

// Function return type inference
function add(a: number, b: number) {
  return a + b; // return type inferred as number
}

// const assertions for literal types
const x = 10;        // number (widened)
const y = "hello";   // string (widened)
const z = { a: 1 };  // { a: number }

const x2 = 10 as const;       // 10 (literal type)
const y2 = "hello" as const;  // "hello" (literal type)
const z2 = { a: 1 } as const; // { readonly a: 10 }

// Array with 'as const' becomes a readonly tuple
const colors = ["red", "green", "blue"] as const;
// readonly ["red", "green", "blue"]
type Color = typeof colors[number]; // "red" | "green" | "blue"

Контекстная типизация

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

typescript
// Contextual typing: the expected type influences the inferred type

// Without context, the parameter type is unknown
window.onmousedown = (mouseEvent) => {
  // mouseEvent is inferred as MouseEvent (from onmousedown's type)
  console.log(mouseEvent.button);
};

// Array map: callback params inferred from array type
const nums = [1, 2, 3];
const doubled = nums.map((n) => n * 2); // n is number, result is number[]

// Contextual typing for object literals
interface User {
  name: string;
  age: number;
}
const user: User = {
  name: "Alice", // inferred as string (from User.name)
  age: 30,
};

// Contextual typing with discriminated unions
type Event =
  | { type: "click"; x: number; y: number }
  | { type: "scroll"; scrollTop: number };

function handle(e: Event) {
  if (e.type === "click") {
    console.log(e.x, e.y); // narrowed
  }
}

// Best practice: let inference work for you — don't over-annotate
const nums2 = [1, 2, 3].map((n) => n * 2); // number[] — no annotation needed

Наилучший общий тип (вывод объединения)

При выводе из нескольких значений (например, литералов массивов) TypeScript находит 'наилучший общий тип' — обычно супертип или объединение. Массив [Dog, Cat] выводится как Animal[] (общая база), а не (Dog | Cat)[]. Для объединения аннотируйте явно. Условные return выводят объединение всех веток. Понимание этого помогает предсказывать, когда нужны явные аннотации, а когда достаточно вывода.

typescript
// When inferring from multiple values, TS finds the best common type

// Array of same type: inferred as that type
const nums = [1, 2, 3]; // number[]

// Array of different types: inferred as union
const mixed = [1, "two", true]; // (string | number | boolean)[]

// Array of subclasses: inferred as the common supertype
class Animal { name: string; }
class Dog extends Animal { bark(): void {} }
class Cat extends Animal { meow(): void {} }

const pets = [new Dog(), new Cat()]; // Animal[] (not (Dog | Cat)[])

// To get a union instead, use an explicit type annotation:
const pets2: (Dog | Cat)[] = [new Dog(), new Cat()];

// Or use 'as const' for readonly tuples:
const tuple = [new Dog(), new Cat()] as const;
// readonly [Dog, Cat]

// Return type inference with conditionals
function getValue(flag: boolean) {
  return flag ? 42 : "hello"; // inferred as number | string
}

Анализ потока управления

TypeScript выполняет анализ потока управления — отслеживает, как типы сужаются и расширяются через if/else, return, присваивания и логические операторы. Тип сужается после проверки и остаётся суженным до переприсваивания переменной. Ранние return (guard clauses) особенно эффективны: после 'if (value === null) return' остальная часть функции знает, что value не null. Поэтому код в стиле guard-clauses так хорошо работает с TypeScript.

typescript
// TypeScript tracks types through control flow

function example(value: string | number | null) {
  // After this check, 'value' is string | number in this branch
  if (value === null) {
    return;
  }

  // value is now string | number (null narrowed out)
  console.log(value);

  if (typeof value === "string") {
    return value.toUpperCase(); // value is string
  }

  // value is now number (string and null narrowed out)
  return value.toFixed(2);
}

// Narrowing through assignments
let x: string | number;
x = "hello";
console.log(x.toUpperCase()); // x is string
x = 42;
console.log(x.toFixed(2)); // x is number

// Narrowing with && and ||
function process(input?: string) {
  const value = input && input.trim(); // string | undefined
  const safe = input || "default"; // string (undefined narrowed out)
}

// Narrowing is reset when a variable is reassigned
let v: string | number = "hi";
v.toUpperCase(); // string
v = 42;
// v.toUpperCase(); // Error: number doesn't have toUpperCase

Оператор satisfies (TS 4.9+)

Оператор 'satisfies' (TS 4.9+) проверяет, что значение соответствует типу, сохраняя наиболее специфичный выведенный тип — в отличие от аннотаций типа, которые расширяют. Идеально для конфигураций, карт маршрутов и объектов темы: вы получаете compile-time валидацию структуры, но доступ к свойствам возвращает точный литеральный тип. Комбинируйте с 'as const' для сохранения литералов и структурной валидации.

typescript
// 'satisfies' checks a value matches a type WITHOUT widening it

// Problem: annotation widens the type
const config1: Record<string, string | number> = {
  port: 8080,
  host: "localhost",
};
// config1.port is 'string | number' (widened — lost the literal)

// Solution: 'satisfies' validates but preserves the inferred type
const config2 = {
  port: 8080,
  host: "localhost",
} satisfies Record<string, string | number>;
// config2.port is 'number' (preserved!) but still validated

// Practical: type-safe route configs
type Routes = Record<string, { method: string; handler: () => void }>;

const routes = {
  "/users": { method: "GET", handler: () => fetchUsers() },
  "/posts": { method: "POST", handler: () => createPost() },
} satisfies Routes;

routes["/users"].method; // string (from Routes)
routes["/users"].handler; // () => void (from Routes)

// 'as const' + 'satisfies': literal types + validation
const colors = {
  primary: "#ff0000",
  secondary: "#00ff00",
} as const satisfies Record<string, `#${string}`;
19

Файлы объявлений и расширение модулей

Написание .d.ts файлов объявлений

Файлы .d.ts содержат объявления типов (без реализации) — они описывают типы JavaScript-кода. Используйте 'declare module' для добавления типов нетипизированным npm-пакетам. 'declare global' расширяет глобальные типы вроде Window. Внешние объявления сообщают TypeScript 'это существует во время выполнения, доверься мне'. Так вы интегрируете устаревший JS, браузерные API и переменные, внедряемые при сборке, в систему типов.

typescript
// types/my-module.d.ts — describes the types of a JS library

// Declare a module (for libraries without types)
declare module "untyped-lib" {
  export function greet(name: string): string;
  export const version: string;
  export interface Config {
    timeout: number;
    retries: number;
  }
}

// Ambient declarations for global variables
declare global {
  interface Window {
    myCustomProp: string;
    myApp: { init: () => void };
  }
}

// Now window.myCustomProp is typed
// window.myCustomProp; // string

// Declaring a global function
declare function myGlobalFn(x: number): string;

// Declaring a global namespace
declare namespace MyLib {
  function doSomething(): void;
  const version: string;
}

// Use 'declare' for things that exist at runtime but not in TS
// Common for: legacy JS, browser globals, build-time constants

Расширение модулей (расширение существующих типов)

Расширение модулей расширяет существующие типы из других модулей — добавляет свойства к интерфейсам без изменения исходного кода. Так middleware Express (например, passport) добавляет req.user, и так вы расширяете типы сторонних библиотек. Синтаксис 'declare module' reopen'ит пространство типов модуля. Расширения должны быть в модуле (файл с import/export), чтобы действовать глобально.

typescript
// Module augmentation: add to an existing module's types

// Extend an interface from another module
import express from "express";

declare module "express" {
  interface Request {
    user?: {
      id: number;
      name: string;
    };
    // Now req.user is typed on all Express requests
  }
}

// Augment a third-party module
declare module "axios" {
  export interface AxiosRequestConfig {
    retryCount?: number; // add a custom config option
  }
}

// Augment a global interface
declare global {
  interface Array<T> {
    last(): T | undefined; // add a custom array method
  }
}

// Implementation (in a .ts file, not .d.ts)
Array.prototype.last = function () {
  return this[this.length - 1];
};

[1, 2, 3].last(); // 3 — now typed!

// Module augmentation is how middleware adds typed properties
// to req/res objects in Express, Fastify, etc.

Директивы тройного слеша

Директивы тройного слеша (///) — специальные комментарии компилятора, указывающие TypeScript включить дополнительные файлы или пакеты типов. Наиболее распространённая — /// <reference types='node' /> для включения @types/node. С современными опциями tsconfig.json 'types' и 'lib' они редко нужны — предпочитайте настройки на основе конфигурации. Они в основном встречаются в .d.ts файлах и устаревшем коде. Понимание их помогает при чтении файлов объявлений.

typescript
// Triple-slash directives are special comments processed by TS

/// <reference path="./other-types.d.ts" />
// Includes another declaration file (rarely needed with modules)

/// <reference types="node" />
// Includes types from @types/node (like process, Buffer, __dirname)

/// <reference lib="es2020" />
// Includes a built-in lib (alternative to tsconfig "lib")

// Most common use: referencing @types packages
// In a .d.ts file for a package that needs Node types:
/// <reference types="node" />

declare function readFile(path: string): Buffer; // Buffer from @types/node

// NOTE: With modern TS and tsconfig "types" and "lib" options,
// triple-slash directives are rarely needed. They're mostly
// used in .d.ts files for backward compatibility.

// Prefer tsconfig.json settings:
// {
//   "compilerOptions": {
//     "types": ["node"],      // instead of /// <reference types="node" />
//     "lib": ["es2020", "dom"] // instead of /// <reference lib="es2020" />
//   }
// }

Публикация типов с пакетом

Для публикации TypeScript-типов с вашим npm-пакетом установите 'types' в package.json, указывающий на ваш .d.ts файл, и включите 'declaration: true' в tsconfig. Потребители автоматически получают типы при установке вашего пакета. declarationMap включает 'Go to Definition' для перехода к исходному .ts файлу. Для библиотек без встроенных типов проект DefinitelyTyped (@types/package) предоставляет объявления, поддерживаемые сообществом.

typescript
// package.json for a library with TypeScript types
{
  "name": "my-library",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",  // <-- points to the type declarations
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  }
}

// tsconfig.json for building a library
{
  "compilerOptions": {
    "declaration": true,        // generate .d.ts files
    "declarationMap": true,     // generate .d.ts.map (source maps for types)
    "sourceMap": true,          // generate .js.map
    "outDir": "./dist",
    "rootDir": "./src",
    "composite": true           // enable project references
  }
}

// Consumers get types automatically when they 'npm install my-library'
import { myFunction } from "my-library"; // fully typed!

// For libraries without bundled types, install @types package:
// npm install --save-dev @types/express

// Check if types exist: https://www.typescriptlang.org/dt/search

Импорт и экспорт только типов

'import type' импортирует только типовую информацию — полностью стирается при выполнении, уменьшая размер bundle и избегая циклических зависимостей. Используйте для интерфейсов, type aliases и повторного экспорта только типов. Инлайн-синтаксис 'import { x, type Y }' (TS 4.5+) смешивает импорты значений и типов. verbatimModuleSyntax (TS 5.0+) строго это требует. Предпочитайте 'import type', когда импортируете что-то, используемое только в позициях типов.

typescript
// 'import type' imports only types (erased at runtime, no JS emitted)
import type { User, Config } from "./types";
import type { ReactNode } from "react";

// These are erased completely — no runtime import in the output JS
// Useful for: reducing bundle size, avoiding circular dependencies

// Mixed import: value + type
import { createStore, type Store } from "redux";
// createStore is a runtime import; Store is type-only

// 'export type' re-exports types only
export type { User, Config } from "./types";

// Interface vs Type for declarations
interface IUser { name: string; }  // can be augmented/merged
type TUser = { name: string };     // cannot be merged, more flexible

// VerbatimModuleSyntax (TS 5.0+): enforces 'import type' for type-only
// imports, ensuring they're always elided
// {
//   "compilerOptions": { "verbatimModuleSyntax": true }
// }
20

Опции tsconfig.json

Основные опции компилятора

tsconfig.json управляет компиляцией TypeScript. 'target' устанавливает выходную версию JS; 'module' — систему модулей. 'strict: true' — самая важная настройка, она включает все строгие проверки типов (noImplicitAny, strictNullChecks и т.д.). 'lib' определяет, какие встроенные API доступны (DOM для браузера, ES2022 для современных JS-функций). Всегда начинайте новые проекты с strict: true.

typescript
{
  "compilerOptions": {
    "target": "ES2022",        // JS version to compile to
    "module": "ESNext",        // module system (ESNext, CommonJS, etc.)
    "moduleResolution": "bundler", // how modules are resolved
    "lib": ["ES2022", "DOM"],  // available type definitions
    "outDir": "./dist",        // output directory for compiled JS
    "rootDir": "./src",        // root of source files
    "sourceMap": true,         // generate .map files for debugging
    "declaration": true,       // generate .d.ts files (for libraries)
    "removeComments": true,    // strip comments from output

    // Type checking strictness
    "strict": true,            // enable ALL strict checks (recommended)
    "noImplicitAny": true,     // error on implicit 'any'
    "strictNullChecks": true,  // null/undefined are separate types
    "noUnusedLocals": true,    // error on unused local variables
    "noUnusedParameters": true,// error on unused function params
    "noImplicitReturns": true, // error if not all paths return
    "noFallthroughCasesInSwitch": true,

    "esModuleInterop": true,   // allow default imports from CommonJS
    "skipLibCheck": true,      // skip type checking of .d.ts files
    "forceConsistentCasingInFileNames": true
  }
}

Флаги строгого режима

Строгий режим — это набор флагов строгости. strictNullChecks — самый влиятельный — он делает null/undefined отдельными типами, заставляя явно их обрабатывать (источник №1 runtime-крашей). noImplicitAny предотвращает тихую эрозию типов. strictPropertyInitialization отлавливает неинициализированные поля класса (используйте ! для определённого присваивания или инициализируйте в конструкторе). Всегда включайте строгий режим в новых проектах — первоначальные затраты оправданы безопасностью.

typescript
{
  "compilerOptions": {
    // 'strict: true' enables ALL of these:

    "strictNullChecks": true,
    // null and undefined are NOT assignable to other types
    // without explicit union. Forces null handling.
    let x: string = null; // ERROR (without this, it's allowed)

    "noImplicitAny": true,
    // Parameters/variables can't be implicitly 'any'
    function fn(x) { } // ERROR: x is implicitly any

    "strictFunctionTypes": true,
    // Function parameter types checked contravariantly
    // (catches unsafe function assignments)

    "strictBindCallApply": true,
    // bind/call/apply are type-checked

    "strictPropertyInitialization": true,
    // Class properties must be initialized or declared with !
    class User {
      name: string; // ERROR: not initialized
      name2!: string; // OK: definite assignment assertion
    }

    "noImplicitThis": true,
    // 'this' must have a known type (no implicit any)

    "alwaysStrict": true
    // Emit "use strict" in every file
  }
}

Стратегии разрешения модулей

moduleResolution управляет разрешением путей импорта. 'node' — классическая стратегия; 'bundler' (TS 5.0+) соответствует современным бандлерам вроде Vite и поддерживает exports из package.json. 'nodenext' — строгий ESM (требует расширений). paths создаёт алиасы импортов (@/ → src/), которые должны быть отражены в конфигурации бандлера (например, resolve.alias в Vite). baseUrl + paths — стандартный способ избежать глубоких относительных импортов (../../../).

typescript
{
  "compilerOptions": {
    // How TS resolves import paths

    "moduleResolution": "node",    // classic Node.js resolution
    // Looks for: file.ts, file/index.ts, node_modules/file

    "moduleResolution": "bundler", // for Vite/webpack/esbuild (TS 5.0+)
    // Matches how bundlers resolve: supports import maps,
    // conditional exports, no file extension requirement

    "moduleResolution": "nodenext", // Node.js ESM resolution (strict)
    // Requires file extensions in imports: import "./foo.js"

    // Path mapping (aliases)
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],           // import "@/components/Button"
      "@utils/*": ["src/utils/*"],
      "@components": ["src/components/index.ts"]
    }

    // rootDirs: virtual directories that map to the same location
    "rootDirs": ["src", "generated"],
    // imports between src/ and generated/ resolve as if same dir
  }
}

// In your code:
import { Button } from "@/components/Button";
import { formatDate } from "@utils/date";
// These resolve to src/components/Button.ts and src/utils/date.ts

Ссылки на проекты (Monorepos)

Ссылки на проекты разбивают большой кодбейз на независимо компилируемые подпроекты — существенно для monorepos. Каждый проект имеет composite: true и эмитит объявления. References объявляют зависимости между проектами. tsc --build (-b) компилирует в порядке зависимостей, пересобирая только изменившееся. Это значительно ускоряет type-checking для больших кодбейзов и обеспечивает архитектурные границы между пакетами.

typescript
// tsconfig.json (root — references sub-projects)
{
  "files": [],
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/api" },
    { "path": "./packages/web" }
  ]
}

// packages/shared/tsconfig.json
{
  "compilerOptions": {
    "composite": true,          // required for project references
    "declaration": true,        // must emit declarations
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"]
}

// packages/api/tsconfig.json
{
  "compilerOptions": { /* ... */ },
  "references": [
    { "path": "../shared" }     // depends on shared
  ]
}

// Benefits:
// 1. Faster builds — only rebuild changed projects
// 2. Clear dependency boundaries between packages
// 3. Type-checking is scoped per project
// 4. tsc --build handles the build order automatically

// Build command: tsc --build (or tsc -b)

Распространённые рецепты tsconfig

Разные типы проектов требуют разных настроек tsconfig. React/Vite использует jsx: 'react-jsx' и noEmit (Vite компилирует). Node.js использует CommonJS (или NodeNext для ESM) и types: ['node']. Библиотеки требуют declaration: true для вывода .d.ts и более низкий target для большей совместимости. isolatedModules требуется Vite/esbuild (каждый файл должен компилироваться независимо). Всегда исключайте тестовые файлы и node_modules из сборки.

typescript
// React + Vite project
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",          // React 17+ JSX transform
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "strict": true,
    "noEmit": true,             // Vite handles compilation
    "isolatedModules": true,    // required by Vite/esbuild
    "verbatimModuleSyntax": true
  },
  "include": ["src"]
}

// Node.js backend project
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",        // or NodeNext for ESM
    "moduleResolution": "node",
    "lib": ["ES2022"],
    "types": ["node"],           // @types/node
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src"]
}

// Library project (publishes to npm)
{
  "compilerOptions": {
    "target": "ES2020",          // broader compatibility
    "module": "ESNext",
    "declaration": true,         // emit .d.ts
    "declarationMap": true,
    "outDir": "./dist",
    "strict": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}
21

Декораторы

Декоратор класса

Декораторы классов получают конструктор и могут вернуть изменённый класс. Они экспериментальные (требуют experimentalDecorators: true). Распространены в NestJS и TypeORM.

typescript
function logged<T extends new (...args: any[]) => any>(con: T) {
    return class extends con {
        created = new Date().toISOString();
    };
}
@logged
class Service { constructor(public name: string) {} }

Декоратор метода

Декораторы методов получают (target, key, descriptor). Обёртывание descriptor.value обеспечивает логирование, кэширование, валидацию. Так работают перехватчики NestJS.

typescript
function log(target: any, key: string, desc: PropertyDescriptor) {
    const orig = desc.value;
    desc.value = function(...args: any[]) {
        console.log(`Calling ${key}`, args);
        return orig.apply(this, args);
    };
}
class Calc { @log add(a: number, b: number) { return a + b; } }

Декоратор свойства

Декораторы свойств получают (target, key). Использование Object.defineProperty создаёт геттеры/сеттеры для валидации. Используется в class-validator для валидации DTO.

typescript
function required(target: any, key: string) {
    let val: any;
    Object.defineProperty(target, key, {
        get() { return val; },
        set(v: any) { if (v == null) throw new Error(`${key} required`); val = v; }
    });
}

Декоратор параметра

Декораторы параметров получают (target, methodKey, parameterIndex). Используются с metadata reflection для валидации. class-validator и NestJS используют это.

typescript
function Min(min: number) {
    return (target: any, key: string, idx: number) => {
        console.log(`${key} param ${idx} >= ${min}`);
    };
}
class Order { create(@Min(0) qty: number) { return qty; } }

Фабрика декораторов

Фабрики декораторов возвращают функцию-декоратор, обеспечивая конфигурацию. Внешняя функция получает параметры, внутренняя — фактический декоратор.

typescript
function Retry(times: number) {
    return function(target: any, key: string, desc: PropertyDescriptor) {
        const orig = desc.value;
        desc.value = async function(...args: any[]) {
            for (let i = 0; i < times; i++)
                try { return await orig.apply(this, args); }
                catch (e) { if (i === times - 1) throw e; }
        };
    };
}
22

Расширение модулей

Расширение встроенных типов

Расширение модулей расширяет существующие типы. declare global позволяет расширять встроенные типы вроде Array. Также должна быть предоставлена runtime-реализация.

typescript
declare global {
    interface Array<T> {
        last(): T | undefined;
        chunk(size: number): T[][];
    }
}
Array.prototype.last = function() { return this[this.length - 1]; };

Расширение типов библиотек

Расширение модулей расширяет типы из сторонних библиотек. declare module reopen'ит тип модуля. Существенно для добавления пользовательских свойств к объектам фреймворка.

typescript
declare module 'express' {
    interface Request {
        user?: { id: string; role: string };
    }
}
app.get('/profile', (req, res) => {
    const userId = req.user?.id;  // Typed!
});

Расширение Window

Расширение Window добавляет пользовательские глобальные свойства с типобезопасностью. Полезно для предоставления доступа к состоянию приложения инструментам отладки или аналитики.

typescript
declare global {
    interface Window {
        myApp: { init: () => void; version: string };
    }
}
window.myApp = { init: () => console.log('Ready'), version: '1.0.0' };

CSS Modules

CSS Modules требуют объявления типов. Объявление отображает импорты .module.css в запись имён классов. Обеспечивает автодополнение для ссылок на CSS-классы.

typescript
declare module '*.module.css' {
    const classes: { readonly [key: string]: string };
    export default classes;
}
import styles from './Button.module.css';
<button className={styles.button} />

Vue Plugin

Vue и другие фреймворки используют расширение модулей для типизации плагинов. ComponentCustomProperties добавляет свойства экземпляра. Обеспечивает типобезопасные плагины.

typescript
declare module 'vue' {
    interface ComponentCustomProperties {
        $auth: { login: () => Promise<void> };
    }
}
export default defineComponent({
    methods: { async login() { await this.$auth.login(); } }
});
23

Слияние объявлений

Слияние интерфейсов

Интерфейсы с одинаковым именем автоматически объединяются. Все члены становятся частью единого интерфейса. Полезно для разделения интерфейсов по файлам.

typescript
interface User { name: string; }
interface User { age: number; }
interface User { email: string; }
const user: User = { name: 'Alice', age: 30, email: '[email protected]' };

Слияние пространств имён

Пространства имён с одинаковым именем объединяют свои экспорты. Это позволяет разделять содержимое пространств имён по файлам. Для нового кода предпочтительнее ES модули.

typescript
namespace Utils {
    export function log(msg: string) { console.log(msg); }
}
namespace Utils {
    export function warn(msg: string) { console.warn(msg); }
}
Utils.log('info'); Utils.warn('alert');

Пространство имён с функцией

Пространства имён могут объединяться с функциями, классами и enum'ами. Пространство имён добавляет статические свойства к функции. Используется в Moment.js и подобных библиотеках.

typescript
function Counter() { Counter.count++; }
namespace Counter {
    export let count = 0;
    export function reset() { count = 0; }
}
Counter(); Counter();
console.log(Counter.count);  // 2

Слияние с классами

Слияние пространства имён с классом добавляет статические члены и вложенные типы. Пространство имён может экспортировать интерфейсы, становящиеся вложенными типами.

typescript
class Settings { static defaults = { theme: 'light' }; }
namespace Settings {
    export interface Options { theme: string; lang: string; }
}
const opts: Settings.Options = { theme: 'dark', lang: 'en' };

Запрещённые слияния

Классы нельзя объединять с другими классами. Переменные нельзя объединять. Функции объединяются как перегрузки. Enum'ы можно объединять с пространствами имён.

typescript
// Cannot merge classes
class A { x = 1; }
class A { y = 2; }  // Error
// Function overloads (allowed)
function fn(x: string): string;
function fn(x: number): number;
function fn(x: any): any { return x; }
24

Сужение типов

typeof и instanceof

typeof сужает примитивные типы. instanceof сужает типы классов. TypeScript понимает эти проверки и сужает тип в каждой ветке.

typescript
function process(value: string | number | Date) {
    if (typeof value === 'string') return value.toUpperCase();
    if (typeof value === 'number') return value.toFixed(2);
    if (value instanceof Date) return value.toISOString();
}

Оператор in

Оператор in проверяет существование свойства, сужая тип. Полезен для discriminated unions с разными именами свойств.

typescript
interface Cat { meow(): void; }
interface Dog { bark(): void; }
function speak(animal: Cat | Dog) {
    if ('meow' in animal) animal.meow();
    else animal.bark();
}

Discriminated Unions

Discriminated unions используют общее свойство (дискриминант) для сужения типов. switch по дискриминанту обеспечивает исчерпывающую проверку. Самый безопасный паттерн для вариантных типов.

typescript
type Result =
    | { status: 'success'; data: string }
    | { status: 'error'; message: string };
function handle(r: Result) {
    switch (r.status) {
        case 'success': console.log(r.data); break;
        case 'error': console.log(r.message); break;
    }
}

Type Predicates

Type predicates (x is T) позволяют создавать пользовательские функции сужения. Возврат true сужает до T, false — до исключённого типа. TypeScript слепо доверяет предикату.

typescript
function isFish(pet: Fish | Bird): pet is Fish {
    return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
    if (isFish(pet)) pet.swim();
    else pet.fly();
}

Assertion-функции

Assertion-функции выбрасывают исключение при неудаче условия, сужая тип для последующего кода. asserts x is T сужает до T. Устраняет избыточные проверки на null.

typescript
function assertDefined<T>(value: T | null | undefined): asserts value is T {
    if (value == null) throw new Error('Null or undefined');
}
function processUser(user?: User) {
    assertDefined(user);
    console.log(user.name);  // User (not undefined)
}
25

Template Literal типы

Базовые Template Literals

Template literal типы создают строковые паттерны. Они ограничивают строки соответствием шаблону. Обеспечивает типобезопасные паттерны для API-эндпоинтов и имён событий.

typescript
type Greeting = `hello ${string}`;
const g: Greeting = 'hello world';  // OK
type Endpoint = `${'GET' | 'POST'} /api/${string}`;
const ep: Endpoint = 'GET /api/users';

Uppercase и Lowercase

Встроенные intrinsic типы преобразуют строковые литеральные типы. Комбинируйте с template literals для генерации типобезопасных имён событий и констант.

typescript
type Upper = Uppercase<'hello'>;  // 'HELLO'
type Lower = Lowercase<'WORLD'>;  // 'world'
type Cap = Capitalize<'foo'>;     // 'Foo'
type EventName = `on${Capitalize<'click'>}`;  // 'onClick'

Переименование ключей

Переименование ключей (предложение as) преобразует ключи во время mapped типов. Генерирует имена getter/setter из имён свойств. Создаёт типобезопасные API из интерфейсов.

typescript
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; }

Сопоставление строковых паттернов

Template literal типы с infer могут парсить строки при компиляции. Split разбивает строку в кортеж. Обеспечивает типобезопасную манипуляцию строками.

typescript
type Split<S extends string, D extends string> =
    S extends `${infer L}${D}${infer R}` ? [L, ...Split<R, D>] : [S];
type Parts = Split<'a,b,c', ','>;  // ['a', 'b', 'c']

Типизация системы событий

Template literal типы с дженериками создают полностью типобезопасные системы событий. Имя события определяет тип полезной нагрузки. on и emit обеспечивают соответствие типов.

typescript
type EventMap = { click: { x: number }; submit: { value: string } };
class Emitter {
    on<K extends keyof EventMap>(event: K, handler: (e: EventMap[K]) => void) {}
    emit<K extends keyof EventMap>(event: K, data: EventMap[K]) {}
}
em.on('click', e => console.log(e.x));  // Typed!
26

Ключевое слово infer

Извлечение возвращаемого типа

infer объявляет переменную типа внутри условного типа. Захватывает тип в определённой позиции. ReturnType — встроенный эквивалент.

typescript
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() { return { name: 'Alice', age: 30 }; }
type User = ReturnOf<typeof getUser>;  // { name: string; age: number; }

Извлечение типа Promise

infer извлекает внутренний тип Promise. DeepUnwrap рекурсивно разворачивает вложенные Promises. Встроенный Awaited<T> делает это в современном TypeScript.

typescript
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type R = UnwrapPromise<Promise<string>>;  // string
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type D = DeepUnwrap<Promise<Promise<boolean>>>;  // boolean

Извлечение элемента массива

infer E захватывает тип элемента массива. Для кортежей infer может захватывать конкретные позиции. Полезно для работы с дженерик-коллекциями.

typescript
type ElementOf<T> = T extends (infer E)[] ? E : never;
type T1 = ElementOf<string[]>;  // string
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type F = First<[string, number]>;  // string

Извлечение параметров функции

infer P захватывает кортеж параметров функции. Parameters — встроенный эквивалент. Полезно для обёртывания функций с сохранением типов.

typescript
type Params<T> = T extends (...args: infer P) => any ? P : never;
function greet(name: string, age: number) { return ''; }
type P = Params<typeof greet>;  // [string, number]

Множественный infer

Несколько переменных infer могут захватывать разные части типа одновременно. Обеспечивает сложные преобразования типов в одном условном типе.

typescript
type FirstLast<T extends any[]> =
    T extends [infer First, ...any[], infer Last]
        ? { first: First; last: Last } : never;
type R = FirstLast<[1, 2, 3, 4]>;  // { first: 1; last: 4 }
27

Вариантность

Ковариантность

Ковариантность позволяет присвоить Dog[] к Animal[]. Массивы TypeScript ковариантны, но это небезопасно: добавление Animal в Dog[] портит массив.

typescript
class Animal { name: string; }
class Dog extends Animal { breed: string; }
let dogs: Dog[] = [new Dog()];
let animals: Animal[] = dogs;  // OK (covariant)
// animals.push(new Animal());  // Unsafe at runtime

Контравариантность

Контравариантность означает, что функция, принимающая Dog, может использоваться там, где ожидается функция, принимающая Animal. Безопасно, так как Dog-обработчик обрабатывает любое Animal, являющееся Dog.

typescript
type Handler<T> = (arg: T) => void;
let dogHandler: Handler<Dog> = (d) => console.log(d.breed);
let animalHandler: Handler<Animal> = dogHandler;  // OK with strictFunctionTypes

Бивариантность

Синтаксис методов бивариантен. Синтаксис свойств-функций контравариантен с strictFunctionTypes. Методы бивариантны для OO-совместимости.

typescript
interface IFace {
    method(x: Animal): void;  // bivariant
    fn: (x: Animal) => void;  // contravariant
}
class Impl implements IFace {
    method(x: Dog) {}  // OK (bivariant)
    fn = (x: Dog) => {}  // Error (contravariant)
}

Вариантность in/out

TypeScript 4.7+ поддерживает явные аннотации вариантности. in отмечает контравариантность (потребители), out — ковариантность (производители), in out — инвариантность (оба).

typescript
interface Producer<out T> { produce(): T; }
interface Consumer<in T> { consume(value: T): void; }
interface Buffer<in out T> { read(): T; write(value: T): void; }

Инвариантные типы

Инвариантные типы требуют точного соответствия типов. Тип инвариантен, когда появляется и во входной, и в выходной позициях. Box<Dog> нельзя присвоить Box<Animal>.

typescript
interface Box<T> { get(): T; set(value: T): void; }
let dogBox: Box<Dog> = {} as any;
let animalBox: Box<Animal> = dogBox;  // Error: invariant
28

Паттерн Builder

Fluent Builder

Паттерн builder конструирует сложные объекты шаг за шагом. Каждый метод возвращает this для цепочечного вызова. Полезен для SQL-запросов, HTTP-запросов и конфигурации.

typescript
class QueryBuilder {
    private parts: string[] = [];
    select(cols: string): this { this.parts.push(`SELECT ${cols}`); return this; }
    from(table: string): this { this.parts.push(`FROM ${table}`); return this; }
    where(cond: string): this { this.parts.push(`WHERE ${cond}`); return this; }
    build(): string { return this.parts.join(' '); }
}

Типобезопасный Builder

Типобезопасные builder'ы используют условные типы для обязательных полей. build() возвращает Person только когда hasName равно true. Отлавливает пропущенные поля при компиляции.

typescript
interface State { hasName: boolean; }
class Builder<S extends State> {
    name(n: string): Builder<S & { hasName: true }> { return this as any; }
    build(): S extends { hasName: true } ? Person : never { return {} as any; }
}

Неизменяемый Builder

Неизменяемые builder'ы создают новый экземпляр для каждой модификации. Система типов отслеживает все добавленные ключи через типы пересечения. Каждый set возвращает новый тип builder'а.

typescript
class ImmutableBuilder<T extends object> {
    constructor(private data: T) {}
    set<K extends string, V>(key: K, value: V):
        ImmutableBuilder<T & { [P in K]: V }> {
        return new ImmutableBuilder({ ...this.data, [key]: value });
    }
    build(): T { return this.data; }
}

Паттерн Director

Director инкапсулирует распространённые последовательности конструирования. Он использует builder для создания стандартных продуктов. Разные director'ы производят разные вариации.

typescript
class HTMLBuilder {
    private html = '';
    addTag(tag: string, content: string): this {
        this.html += `<${tag}>${content}</${tag}>`; return this;
    }
    build(): string { return this.html; }
}
class Director {
    buildPage(title: string, body: string): string {
        return new HTMLBuilder().addTag('title', title).addTag('body', body).build();
    }
}

Step Builder

Step builder обеспечивает определённый порядок вызова методов через систему типов. Каждый шаг возвращает другой тип только со следующим доступным методом.

typescript
class Step1 { name(n: string): Step2 { return new Step2(n); } }
class Step2 {
    constructor(private name: string) {}
    age(a: number): Step3 { return new Step3(this.name, a); }
}
class Step3 {
    constructor(private name: string, private age: number) {}
    build() { return { name: this.name, age: this.age }; }
}
29

Тестирование с TypeScript

Jest с TypeScript

Используйте ts-jest или @swc/jest для тестов TypeScript. describe группирует связанные тесты, it определяет тестовые случаи. expect создаёт утверждения с матчерами вроде toBe, toEqual.

typescript
import { sum } from './sum';
describe('sum', () => {
    it('adds two numbers', () => {
        expect(sum(1, 2)).toBe(3);
    });
    it('handles negatives', () => {
        expect(sum(-1, -2)).toBe(-3);
    });
});

Тестирование типов

expectTypeOf тестирует типы при компиляции. Проверяет возвращаемые типы, типы параметров и разрешённые типы промисов. Заваливает сборку, если типы неверны.

typescript
import { expectTypeOf } from 'vitest';
type GetUser = (id: string) => Promise<{ name: string }>;
test('return type is correct', () => {
    const fn = {} as unknown as GetUser;
    expectTypeOf(fn).returns.toEqualTypeOf<{ name: string }>();
    expectTypeOf(fn).parameters.toEqualTypeOf<[string]>();
});

Мокирование с типами

jest.Mocked<T> создаёт типизированный мок из интерфейса. jest.fn() создаёт мок-функции с типизированными возвращаемыми значениями. Мок полностью типизирован.

typescript
interface Database { find(id: string): Promise<User>; }
const mockDb: jest.Mocked<Database> = {
    find: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
};
mockDb.find.mockResolvedValueOnce({ id: '2', name: 'Bob' });

Setup и Teardown тестов

beforeAll запускается один раз перед всеми тестами, afterAll — один раз после. beforeEach запускается перед каждым тестом, afterEach — после. Используйте для setup и очистки.

typescript
describe('Database', () => {
    beforeAll(async () => { db = createDatabase(); await db.connect(); });
    afterAll(async () => { await db.disconnect(); });
    beforeEach(async () => { await db.clear(); });
    afterEach(() => { jest.restoreAllMocks(); });
});

Свойство-ориентированное тестирование

Свойство-ориентированное тестирование генерирует случайные входные данные для проверки инвариантов. fc.assert запускает свойство несколько раз. Отлавливает краевые случаи, пропускаемые пример-ориентированными тестами.

typescript
import { fc } from 'fast-check';
describe('sort', () => {
    it('preserves length', () => {
        fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
            return [...arr].sort().length === arr.length;
        }));
    });
});
30

Распространённые подводные камни

any vs unknown

any отключает проверку типов, скрывая баги. unknown типобезопасен: вы должны сузить его перед использованием. Используйте unknown для ненадёжных источников (API, JSON.parse).

typescript
// BAD: any disables type checking
function bad(data: any) { return data.foo.bar; }
// GOOD: unknown forces type checking
function good(data: unknown) {
    if (typeof data === 'object' && data !== null && 'foo' in data)
        return (data as any).foo;
}

Проверки избыточных свойств

TypeScript проверяет избыточные свойства только у объектных литералов при прямом присваивании. Через переменную проверка пропускается. Используйте zod для более строгой runtime-валидации.

typescript
interface User { name: string; age: number; }
// Direct literal: checked
const u1: User = { name: 'A', age: 30, extra: true };  // Error
// Via variable: not checked
const data = { name: 'A', age: 30, extra: true };
const u2: User = data;  // OK

Enum vs Union

Enum'ы создают runtime-объекты с обратным отображением. Типы объединения не имеют runtime-накладных расходов и поддерживают tree-shaking. Для нового кода предпочитайте типы объединения.

typescript
// Enum: runtime object
enum Color { Red, Green, Blue }
// Union: no runtime
type Color2 = 'red' | 'green' | 'blue';
// Const enum: erased
const enum Dir { Up, Down }

Структурная типизация

TypeScript использует структурную типизацию: типы совместимы, если совпадают формы. Admin присваиваем User. Это может вызывать логические баги. Branded типы добавляют номинальное различие.

typescript
interface User { name: string; age: number; }
interface Admin { name: string; age: number; role: string; }
const admin: Admin = { name: 'Bob', age: 40, role: 'admin' };
const user: User = admin;  // OK (structural)

Опасности утверждений типа

Утверждения типа (as) переопределяют TypeScript без runtime-проверок. Используйте runtime-валидацию (zod, io-ts) для внешних данных. safeParse возвращает результат без выбрасывания исключения.

typescript
// BAD: assertion hides errors
const user = JSON.parse(input) as User;
// GOOD: runtime validation
import { z } from 'zod';
const schema = z.object({ name: z.string() });
const result = schema.safeParse(JSON.parse(input));
if (result.success) { const user: User = result.data; }

Связанные сниппеты TypeScript

Copy-paste ready code for common tasks.

Дженерик-функции

Определение и использование дженерик-функций.

Условные типы

Выбор типов на основе условий.

Отображаемые типы

Конструирование новых типов из существующих.

Служебные типы

Встроенные служебные типы TypeScript.

Type Guards

Пользовательские функции проверки типов.

Перегрузки функций

Определение сигнатур перегрузки функций.

Декораторы

Декораторы классов и методов.

Enum

Числовые, строковые и const enum.

Наследование интерфейсов

Наследование и реализация интерфейсов.

Абстрактные классы

Определение абстрактных классов и абстрактных методов.

Пространства имён

Организация кода с помощью пространств имён.

Объявления модулей

Написание объявлений типов для JS-библиотек.

Слияние объявлений

Слияние нескольких объявлений с одинаковым именем.

Опциональная цепочка

Безопасный доступ к глубоким свойствам.

Nullish-объединение

Использование значения по умолчанию только для null/undefined.

Вывод типов

TypeScript автоматически выводит типы.

const-утверждения

Сужение типов с помощью as const.

Оператор satisfies

Проверка типа с сохранением самого узкого типа.

Ключевое слово infer

Извлечение типов внутри условных типов.

Типы шаблонных литералов

Конструирование типов на основе строк.

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.