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

Типы данных и типизация в TypeScript

Разработчику Архитектору

Дальше: Переменные · Функции · Дженерики · Справочник — основы и объединения


TypeScript добавляет к JavaScript статическую проверку типов: ошибки несовместимости данных ловятся до запуска, IDE подсказывает поля и методы по контракту. Удобный порядок чтения — первая программа, эта статья, переменные, функции, классы.

Сначала: типы в JavaScript — динамическая модель, с которой сравнивают TS. Общие определения — типы данных и типизация в базовом разделе.

Блоки справочника 301 в этой статье

Блок Справочник по TypeScriptГде в этой статье
§1 Основы типизациипримитивы, unknown, вывод, сужение типов (narrowing)
§3 Объединения и пересеченияunion/intersection
§7 Утилитарные типыUtility types
§2 Составные типымассивы, кортежи здесь; Map/RecordКоллекции и массивы в TypeScript

Полная карта 18 блоков — индекс раздела.


JavaScript и TypeScript

Play ITЗагрузка интерактивного демо…

Play ITЗагрузка интерактивного демо…

Play ITЗагрузка интерактивного демо…

— одна среда, два уровня

JavaScript — динамически типизирован: тип привязан к значению в runtime. TypeScript — статическая надстройка: компилятор анализирует код и выдаёт JavaScript; типы в .js не попадают.

JavaScriptTypeScript
Когда проверяются типыПри выполненииПри компиляции (tsc, IDE)
Ошибка строка + объектМожет всплыть в runtimeЧасто видна в редакторе сразу
Справочник по синтаксису типовСправочник по TypeScript

TypeScript включает типы значений JS (number, string, object, …) и добавляет свои — unknown, union (объединение типов через |), utility-типы, дженерики. Развёрнутое сравнение JS и TS с примерами и критериями выбора — Сравнение JavaScript и TypeScript. Обзор в контексте курса JS — Обзор TypeScript в JS; оглавление раздела — О разделе.


TypeScript и типичные проблемы JavaScript

В JavaScript тип привязан к значению в момент выполнения. В больших кодовых базах это даёт типичные боли:

Проблема JSКак помогает TS
Опечатка в имени поля (user.nmae)Ошибка в IDE и при tsc
Несовместимые аргументы функцииПроверка сигнатуры на всех вызовах
Рефакторинг API (переименование поля)Поиск всех мест по проекту
"Тихий" undefined / nullstrictNullChecks, union, сужение типов
Внешний JSON без схемыunknown + guard или Zod — Рекомендации по разработке на TypeScript

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


Статическая типизация

Статическая типизация означает: компилятор (и tsserver в IDE) строит модель типов по исходникам и сообщает о несовместимости до node или открытия страницы в браузере.

Динамика (JS)Статика (TS)
Когда видна ошибкаЧасто в runtimeНа этапе разработки / CI
Где живут типыУ значенияВ анализе кода; в .js не попадают
Изменение контрактаМожет "пройти" до тестаПодсветка всех вызовов

Проверку выполняют инструменты TypeScript (tsc, языковой сервер), движок JavaScript типы не анализирует — см. "Где выполняется проверка типов" и Компиляция.


Типобезопасность (type safety)

Типобезопасность — свойство кода: операции применяются только к значениям, совместимым с контрактом типа. В строгом TS это означает:

  • меньше неявных приведений и "магии" any;
  • явные ветки для null / undefined;
  • сужение типа (narrowing) перед доступом к полям unknown;

Полная безопасность в production достигается слоями — TS + тесты + runtime-валидация границ (HTTP, формы, localStorage). TS закрывает ошибки логики типов в коде; Zod/Valibot — несоответствие внешних данных.


Структурная типизация

TypeScript использует структурную (structural) типизацию: совместимость определяется набором полей и методов, без учёта имени типа (в отличие от номинальной модели Java/C#).

type Point = { x: number; y: number };

function draw(p: Point) {
console.log(p.x, p.y);
}

// Отдельный type alias — но форма та же
draw({ x: 1, y: 2 }); // OK

Функция ожидает объект с полями x и y типа number. Подойдёт любой объект с такой формой, даже без отдельного alias Point. Это близко к утиной типизации в JS, только проверка идёт до запуска.

Утиная типизация в JS и структурная в TS

JavaScript (утиная)TypeScript (структурная)
Критерий совместимости"Если ходит как утка…" — есть нужные поля/методыТа же идея формы, но проверяется компилятором
Когда ломаетсяВ runtime при вызовеНа этапе tsc / в IDE
Имена типовНе важныИмена interface / type — для людей; совместимость по полям

В номинальных языках (Java, C#) тип A и тип B с одинаковыми полями — разные, если не объявлено наследование. В TS два alias с одной формой взаимозаменяемы — отсюда важность branded types, когда нужна "номинальность" — Паттерны.

Номинальные типы в TS

Если двум типам с одинаковой формой нужны разные имена в проверке, используют branded types или классы — см. Паттерны и Справочник — распространённые паттерны.


Проектирование типов и принцип надёжности

Хорошие типы в TS — это контракты, которые сложно нарушить случайно:

  1. Узкие union вместо string для кодов состояния ("idle" | "loading" | "error").
  2. Discriminated union с полем kind / type — исчерпывающий switch и neverОператоры и условные ветвления в TypeScript, Паттерны в TypeScript.
  3. Разделение слоёв: DTO (Data Transfer Object — форма данных с API) отдельно от domain-модели (бизнес-логика) — Рекомендации по разработке на TypeScript, TypeScript и Node.js.

Закон подстановки Лискова (LSP)

В ООП LSP: подтип должен без сюрпризов подставляться вместо базового типа. В TypeScript с структурной моделью это проявляется так:

  • объект с дополнительными полями часто совместим с более узким ожиданием (excess property check действует на литералы, не на все случаи);
  • функция с более узким параметром или более широким возвратом может быть несовместима с ожидаемой сигнатурой (контравариантность параметров).
type Animal = { name: string };
type Dog = Animal & { breed: string };

function greet(a: Animal): void {
console.log(a.name);
}

const d: Dog = { name: "Рекс", breed: "овчарка" };
greet(d); // OK: Dog структурно подходит под Animal

Проектируя иерархии, предпочитайте композицию и явные union вместо глубокого наследования — классы, Паттерны. "Надёжность" типов в команде закрепляется strict, code review и запретом нового any без причины.


Типы и значения

УровеньЧто этоПример
ТипКонтракт, проверяется компиляторомinterface User { id: string; name: string }
ЗначениеДанные в runtime (обычный JS)const u: User = { id: "1", name: "Анна" }

Удобный порядок работы: сначала описать контракт (type / interface), затем реализацию. Ошибки в контракте IDE покажет до node или сборки bundler.

type Role = "admin" | "user";

interface Account {
id: string;
role: Role;
}

function canDelete(account: Account): boolean {
return account.role === "admin";
}

const me: Account = { id: "u-42", role: "user" };
console.log(canDelete(me)); // false

Примитивные типы

TypeScript наследует примитивы ECMAScript:

ТипНазначение
stringТекст
numberВсе числа JS, включая NaN, Infinity
booleantrue / false
bigintЦелые произвольной длины
symbolУникальные идентификаторы
nullЯвное отсутствие значения
undefinedНе инициализировано / отсутствует
let title: string = "Отчёт";
let count: number = 10_000;
let ok: boolean = true;

Запись 10_000 — numeric separator для читаемости; значение то же, что 10000. Аннотация : string не даст присвоить число без явного преобразования.


Специальные типы TypeScript

ТипСмыслКогда использовать
voidФункция ничего полезного не возвращаетfunction log(msg: string): void
neverЗначение не возникает (throw, бесконечный цикл)exhaustive switch, guard
anyПроверка типов отключенаLegacy, крайний случай
unknownЛюбое значение, но нельзя трогать без проверкиJSON, catch, внешний ввод

any и unknown — когда что использовать

any — "выключатель" проверки: значение можно присвоить куда угодно, вызвать любой метод, передать в любую функцию. Компилятор пропускает ошибки — поведение как в нетипизированном JavaScript.

unknown — "безопасный контейнер": в переменную можно положить что угодно, но использовать её можно только после сужения типа (narrowing) — проверки через typeof, in, type predicate или утверждения с реальной проверкой.

let a: any = fetchSomething();
a.foo.bar(); // компилятор молчит — ошибка возможна в runtime

let u: unknown = fetchSomething();
// u.foo; // ошибка: Object is of type 'unknown'
if (typeof u === "object" && u !== null && "id" in u) {
const o = u as { id: string };
console.log(o.id);
}
Критерийanyunknown
Присвоение в string, number, …без проверкитолько после narrowing
Вызов методовразрешёнзапрещён до сужения
Передача в типизированную функциюразрешенатолько после сужения
Типичный источникlegacy, @types с дырамиJSON.parse, catch, response.json()

Правило: для внешних данных начинайте с unknown, не с any. Подробнее про валидацию и командные правила — Рекомендации.

Код ITЗагрузка примера кода…

Тип never показывает компилятору, что после throw выполнение не продолжается. unknown заставляет сузить тип перед доступом к полям — это безопаснее, чем any.

Не злоупотребляйте any

Каждый новый any отключает защиту TS. Для API и форм — unknown + проверка или библиотека валидации (Zod, Valibot). Правила проекта — рекомендации.


null, undefined и strictNullChecks

В JavaScript null и undefined часто смешивают. С флагом strictNullChecks (входит в strict) тип string не включает null/undefined — их нужно явно добавить в union:

type Name = string | null;

function greet(name: Name): string {
if (name === null) {
return "Гость";
}
return `Привет, ${name}`;
}

Типичная ошибка компилятора: Object is possibly 'undefined'. Решения:

  • проверка if (x !== undefined);
  • оператор ?? (значение по умолчанию);
  • optional chaining user?.profile?.email;
  • не злоупотреблять non-null assertion ! без реальной проверки.

Объекты — type, interface, опциональность

interface — удобен для объектов и расширения (extends).

type — union, tuple, mapped types, вычисляемые типы.

Сравнение interface и typeСинтаксис §interface и type.

Обязательные и опциональные поля

Обязательное поле — должно присутствовать при создании объекта (если не задано значение по умолчанию в деструктуризации параметра). Опциональное — может отсутствовать; тип поля тогда включает undefined.

ЗаписьСмыслТип поля
id: stringобязательноstring
email?: stringможно не передаватьstring | undefined
readonly createdAt: stringобязательно, нельзя переприсвоитьstring
interface User {
id: string; // обязательное
email: string; // обязательное
nickname?: string; // опциональное
readonly createdAt: string;
}

// OK
const u1: User = { id: "1", email: "a@b.c", createdAt: "2024-01-01" };

// OK — nickname опущен
const u2: User = { id: "2", email: "x@y.z", createdAt: "2024-02-01" };

// Ошибка — нет обязательного email
// const bad: User = { id: "3", createdAt: "..." };

Utility Required<T> делает все поля обязательными; Partial<T> — все опциональными (удобно для PATCH и форм):

type UserPatch = Partial<Pick<User, "email" | "nickname">>;
type UserRow = Required<User>; // nickname тоже обязателен
type PublicUser = Pick<User, "id" | "nickname">;

Разбор:

  • ? — свойство может отсутствовать (undefined).
  • readonly — нельзя переприсвоить поле после создания объекта.
  • Pick — utility-тип: подмножество полей (см. ниже).

Вложенные объекты можно описать inline или вынести в отдельный тип:

interface ServerConfig {
host: string;
port: number;
}

interface AppConfig {
server: ServerConfig;
debug: boolean;
}

Подробнее про interface и typeСинтаксис TypeScript. Таблицы полей — Справочник — интерфейсы.


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

Полный разбор Map, Set, Record и выбор структуры — коллекции (Справочник — составные типы).

const ids: number[] = [1, 2, 3];
const tags: Array<string> = ["ts", "js"];

// Кортеж: фиксированная длина и типы по позициям
type Rgb = [number, number, number];
const red: Rgb = [255, 0, 0];

type HttpResult = [status: number, body: string];

Разбор:

  • number[] и Array<number> эквивалентны.
  • Кортеж [number, number, number] не примет [255, 0] — не хватает элемента.
  • Именованные элементы кортежа (status:) улучшают подсказки IDE.

Коллекции Map, Set, Recordколлекции и массивы, Справочник — составные типы.


Union и intersection

Union (|)объединение типов: значение соответствует одному из перечисленных вариантов.

type Id = string | number;
type Status = "idle" | "loading" | "success" | "error";

function formatId(id: Id): string {
if (typeof id === "string") {
return id.toUpperCase();
}
return id.toFixed(0);
}

Intersection (&) — объединение требований:

type Named = { name: string };
type Timestamped = { createdAt: string };
type Entity = Named & Timestamped;
// { name: string; createdAt: string }

Literal union ("admin" | "user") — основа discriminated union для состояний UI и команд API — операторы и ветвления, Паттерны.


Литеральные типы и enum

Строковые/числовые литералы сужают допустимые значения:

type Theme = "light" | "dark";
const theme: Theme = "light";
// theme = "blue"; // ошибка компиляции

enum — именованный набор констант (в JS генерирует объект):

enum Direction {
Up,
Down,
Left,
Right,
}

enum PaymentStatus {
Pending = "PENDING",
Paid = "PAID",
Failed = "FAILED",
}

В новых проектах часто предпочитают as const + union вместо enum (меньше сюрпризов при сборке). Оба подхода описаны в справочнике.


Вывод типов (type inference)

Вывод типов (type inference) — компилятор сам определяет тип из контекста: правой части присваивания, return, аргументов при вызове generic-функции. Явная аннотация не всегда нужна:

// Тип name выведен как string
const name = "Алиса";

// Тип параметров и возврата выводится из тела
function double(n: number) {
return n * 2;
}

Подробная таблица "когда писать : T" — Переменные — обязательно ли писать аннотацию.

Явно указывайте типы там, где:

  • публичный API модуля (экспортируемые функции);
  • компилятор выводит слишком широкий тип (string вместо "ok" | "fail");
  • пустой массив [] без контекста становится never[].
// Плохо без контекста
const items = []; // never[]

// Лучше
const items: string[] = [];

Слишком широкий тип

Широкий тип — когда выведенный тип допускает больше значений, чем нужно по смыслу. Компилятор не ошибается, но теряется точность контракта.

const status = "ok"; // выведено: string, а не "ok"
// status = "fail"; // компилятор разрешит — опечатка не поймается

const statusLiteral = "ok" as const; // "ok"
// statusLiteral = "fail"; // ошибка

type Result = "ok" | "fail";
const fixed: Result = "ok"; // явный union
СитуацияВыводКак сузить
const x = "a"string (для let) / литерал (для const в простых случаях)as const или явный union
["a", "b"]string[]as const → readonly tuple / union элементов
fetch без схемыunknown / any в legacytype predicate, Zod
Объект конфигаширокие поля при аннотации : Configsatisfies Config — см. ниже

Компилятор намеренно выбирает безопасное обобщение — широкий тип ожидаем. Задача разработчика — сузить контракт там, где важны конкретные литералы.


Сужение типов (narrowing)

Сужение типов (type narrowing) — процесс, при котором после проверки (if, switch, type predicate) компилятор сокращает union до одной ветки и разрешает доступ к полям этой ветки.

После проверки компилятор сужает union до конкретной ветки.

typeof, truthiness

function print(value: string | number): void {
if (typeof value === "string") {
console.log(value.toUpperCase());
return;
}
console.log(value.toFixed(2));
}

in для объектов

type Cat = { kind: "cat"; meow: () => void };
type Dog = { kind: "dog"; bark: () => void };
type Pet = Cat | Dog;

function speak(pet: Pet) {
if (pet.kind === "cat") {
pet.meow();
return;
}
pet.bark();
}

Discriminated union (дискриминируемое объединение)

Discriminated union — union объектов с общим полем-дискриминантом (часто kind, type, status), значение которого однозначно указывает ветку. Компилятор сужает тип по этому полю в if / switch.

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

function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // shape: Circle
case "square":
return shape.side ** 2; // shape: Square
}
}

Требования к хорошему discriminated union:

  1. Дискриминант — литеральный union ("idle" | "loading", не просто string).
  2. В каждой ветке — разный набор полей, зависящий от дискриминанта.
  3. Обработка — switch / if с exhaustive default: never — см. ниже.

Практика в UI и async — Операторы и ветвления, Асинхронность.

Discriminated union и exhaustive check

Код ITЗагрузка примера кода…

Разбор:

  • Поле statusдискриминант: по нему TS знает, какие поля доступны в ветке.
  • default с never ловит забытый вариант union при добавлении нового status.

Type predicates (value is T)

Пользовательская проверка, после которой компилятор доверяет сужению:

Код ITЗагрузка примера кода…

Разбор:

  • Сигнатура value is Paymenttype predicate: внутри if (isPayment(x)) тип xPayment.
  • Отличие от boolean: обычная функция check(x): boolean не сужает тип аргумента.
  • Подробнее в ветвлениях и Асинхронность (ответ fetch).

Assertion functions (asserts value is T)

Если проверка не прошла — функция бросает; если вернулась — тип сужен:

function assertPayment(value: unknown): asserts value is Payment {
if (!isPayment(value)) {
throw new Error("Expected Payment");
}
}

function processWebhook(body: unknown): void {
assertPayment(body);
console.log(body.amount); // Payment
}

Разбор:

  • asserts value is Payment — компилятор считает: после вызова без throw аргумент точно Payment.
  • Удобно в начале обработчика, когда дальше много кода и не хочется оборачивать всё в if.

Пример диагностики до сужения:

error TS18046: 'raw' is of type 'unknown'.

После if (!isPayment(raw)) throw … или assertPayment(raw) ошибка исчезает.


Utility types

Встроенные преобразования над типами. Один и тот же User удобен, чтобы увидеть сценарий → utility:

СценарийUtilityПример
PATCH API, не все поля обязательныPartial<T>Partial<User>
форма/импорт: всё заполненоRequired<T>Required<User> (в т.ч. бывший email?)
конфиг после загрузки, без мутацийReadonly<T>Readonly<User>
виджет сайдбараPick<T, K>Pick<User, "name" | "role">
POST без серверного idOmit<T, K>Omit<User, "id">
кэш id → пользовательRecord<K, V>Record<string, User>
роли без adminExclude<T, U>Exclude<User["role"], "admin">"user"
только пересечение с наборомExtract<T, U>Extract<User["role"], "admin" | "root">"admin"
убрать null/undefined из поляNonNullable<T>NonNullable<User["email"]>string

Базовая таблица:

UtilityНазначение
Partial<T>Все поля опциональны
Required<T>Все поля обязательны
Readonly<T>Все поля readonly
Pick<T, K>Подмножество полей
Omit<T, K>Все поля, кроме перечисленных
Record<K, V>Словарь ключ → значение
Exclude<T, U>Убрать из union типы, совместимые с U
Extract<T, U>Оставить в union только совместимые с U
NonNullable<T>Убрать null и undefined
interface User {
id: string;
name: string;
email?: string;
role: "admin" | "user";
}

type UserPatch = Partial<Pick<User, "name">>;
type NewUserRequest = Omit<User, "id">;
type PublicUser = Omit<User, "role">;
type RoleMap = Record<User["role"], string[]>;
type NonAdminRole = Exclude<User["role"], "admin">; // "user"
type ValidEmail = NonNullable<User["email"]>; // string

Полный список и продвинутые (ReturnType, Parameters, Awaited, mapped types) — Справочник — утилитарные и расширенные типы. Связки generics + Omit + keyof + literal unionдженерики.

Exclude, Extract и NonNullable

Exclude<T, U> вырезает из union T всё, что совместимо с U. Удобно для "все ответы API, кроме успеха" или "все роли, кроме admin".

Extract<T, U> оставляет только пересечение: что из T подходит под U.

NonNullable<T> убирает null и undefined — часто после проверки optional-поля.

Код ITЗагрузка примера кода…

Разбор:

  • Exclude работает на уровне типов (до runtime); сужение внутри logError — на уровне значений через дискриминант statusветвления.
  • После if (e.status === "validation_error") в else остаётся только ServerErrorResponse — доступ к e.code безопасен.
  • Для успешной ветки используйте отдельный тип OkResponse или Extract<ApiResponse, OkResponse>.

Связанный выбор аргументов (ключ → тип значения, событие → payload) — дженерики §связанный выбор.


Где выполняется проверка типов

ЭтапИнструментЧто проверяется
Редактированиеtsserver / LSPФайлы по tsconfig, подсказки, ошибки в реальном времени
Сборка / CItsc, tsc --noEmitВесь граф проекта (или references в monorepo)
RuntimeJavaScriptТипов нет — только значения

Аннотации : string, interface, дженерики стираются при emit. Если в dist/ попал старый JS без пересборки, runtime может вести себя иначе, чем показывает IDE — всегда синхронизируйте сборку и typecheck в CI — Архитектура компиляции TypeScript и runtime.


as const и satisfies

as const

as const (const assertion) — утверждение, что значение неизменяемо и литералы нужно сохранить максимально узкими: readonly, строковые/числовые литералы вместо string/number.

const routes = ["home", "profile", "settings"] as const;
type Route = (typeof routes)[number]; // "home" | "profile" | "settings"

const config = { retries: 3, mode: "strict" } as const;
// config.retries: 3, config.mode: "strict", всё readonly

Без as const массив routes получил бы тип string[] вместо кортежа литералов.

satisfies

satisfies — проверяет, что значение соответствует типу, но не расширяет вывод полей до этого типа. IDE сохраняет узкие литералы и автодополнение.

type Palette = Record<string, string | readonly [number, number, number]>;

const palette = {
red: [255, 0, 0],
green: "#00ff00",
} satisfies Palette;

// palette.green — string (не union всех значений Palette)
const g = palette.green.toUpperCase();
КонструкцияПроверка формыВывод типа переменной
: Paletteдаширокий Palette
satisfies Paletteдаузкий, как у литерала
as constнет (только сужение литералов)максимально узкий readonly

С satisfies форма проверяется, автодополнение по полям остаётся узким. Широкая аннотация : Palette расширила бы тип green до string | readonly [...].


typeof и keyof

const defaults = {
retries: 3,
timeoutMs: 5000,
} as const;

type Defaults = typeof defaults;
// { readonly retries: 3; readonly timeoutMs: 5000 }

type DefaultKey = keyof typeof defaults; // "retries" | "timeoutMs"

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


Частые ошибки

ОшибкаПричинаЧто делать
Type 'X' is not assignable to type 'Y'Контракт не совпадает с даннымиПройти цепочку API → DTO (объект с API) → domain; исправить тип или значение
Object is possibly 'undefined'strictNullChecksПроверка, ??, optional chaining
Property 'x' does not exist on typeUnion не суженswitch по дискриминанту, in, typeof
Везде any ради быстрой сборкиОтключена проверкаunknown, типы API, рекомендации
Дублирование структурыНет общего typeinterface / Pick / общий модуль types/

Практика

  1. Опишите User, Admin, Guest с полем-дискриминантом kind и объедините в Account. Напишите getDisplayName(account: Account) с switch.
  2. Смоделируйте загрузку API как LoadState (idle / loading / success / error) и функцию render(state) с exhaustive default.
  3. Для объекта конфигурации используйте satisfies и убедитесь, что IDE подсказывает конкретные поля.
  4. Возьмите JSON из fetch как unknown и напишите parseUser(raw: unknown): User с проверками (без any).
  5. Сформируйте PublicUser через Omit и UserPatch через Partial<Pick<…>>.
  6. Опишите ApiResponse из трёх веток и тип ErrorResponse = Exclude<ApiResponse, OkResponse>; напишите logError с сужением по status.
  7. Напишите isUser(value: unknown): value is User и assertUser(value: unknown): asserts value is User.

Смежные статьи