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

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

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

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


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

Маршрут: Первая программатипы данныхпеременныефункцииклассы.

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

Эта статья и справочник 301

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

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


JavaScript и TypeScript: одна среда, два уровня

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

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

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


Зачем TypeScript: проблемы JavaScript

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

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

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


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

Статическая типизация означает: компилятор (и 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 и never12.md, 28.md.
  3. Разделение слоёв: DTO (сеть) ≠ domain (бизнес) — 6.md, 22.md.

Закон подстановки Лискова (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, внешний ввод
function fail(message: string): never {
throw new Error(message);
}

function parseUser(raw: unknown): { id: string; name: string } {
if (typeof raw !== "object" || raw === null) {
throw new Error("Expected object");
}
const o = raw as Record<string, unknown>;
if (typeof o.id !== "string" || typeof o.name !== "string") {
throw new Error("Invalid user shape");
}
return { id: o.id, name: o.name };
}

Тип 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 User {
id: string;
email: string;
nickname?: string; // опционально
readonly createdAt: string;
}

type PublicUser = Pick<User, "id" | "nickname">;

Разбор:

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

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

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

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

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


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

Полный разбор 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)

Явная аннотация не всегда нужна:

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

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

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

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

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

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

После проверки компилятор сужает 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 и exhaustive check

type LoadState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string }
| { status: "error"; message: string };

function render(state: LoadState): string {
switch (state.status) {
case "idle":
return 'Нажмите "Загрузить"';
case "loading":
return "Загрузка…";
case "success":
return state.data;
case "error":
return state.message;
default: {
const _exhaustive: never = state;
return _exhaustive;
}
}
}

Разбор:

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

Type predicates (value is T)

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

type Payment = { id: string; amount: number };

function isPayment(value: unknown): value is Payment {
if (typeof value !== "object" || value === null) return false;
const o = value as Record<string, unknown>;
return typeof o.id === "string" && typeof o.amount === "number";
}

function charge(raw: unknown): void {
if (!isPayment(raw)) {
throw new Error("Invalid payment payload");
}
console.log(raw.amount); // Payment, не unknown
}

Разбор:

  • Сигнатура 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-поля.

type OkResponse = { status: "ok"; data: unknown };
type ValidationErrorResponse = {
status: "validation_error";
fields: string[];
};
type ServerErrorResponse = { status: "server_error"; code: number };

type ApiResponse =
| OkResponse
| ValidationErrorResponse
| ServerErrorResponse;

type ErrorResponse = Exclude<ApiResponse, OkResponse>;
// ValidationErrorResponse | ServerErrorResponse

function logError(e: ErrorResponse): string[] | number {
if (e.status === "validation_error") {
return e.fields;
}
return e.code;
}

Разбор:

  • 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 — 15.md.


as const и satisfies

as const делает литералы и объекты максимально узкими (readonly, literal types):

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

satisfies проверяет соответствие типу, сохраняя вывод полей:

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();

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


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 → 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.

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