Типы данных и типизация в TypeScript
Дальше: Переменные · Функции · Дженерики · Справочник — основы и объединения
TypeScript добавляет к JavaScript статическую проверку типов: ошибки несовместимости данных ловятся до запуска, а IDE подсказывает поля и методы по контракту.
Маршрут: Первая программа → типы данных → переменные → функции → классы.
Сначала: типы в JavaScript — динамическая модель, с которой TS сравнивают. Общие определения — типы данных и типизация в базовом разделе.
Эта статья и справочник 301
| Блок 301 | Где в этой статье |
|---|---|
| §1 Основы типизации | примитивы, unknown, вывод, narrowing |
| §3 Объединения и пересечения | union/intersection |
| §7 Утилитарные типы | Utility types |
| §2 Составные типы | массивы, кортежи здесь; Map/Record — 19.md |
Полная карта 18 блоков — индекс раздела.
JavaScript и TypeScript: одна среда, два уровня
JavaScript — динамически типизирован: тип привязан к значению в runtime. TypeScript — статическая надстройка: компилятор анализирует код и выдаёт JavaScript; типы в .js не попадают.
| JavaScript | TypeScript | |
|---|---|---|
| Когда проверяются типы | При выполнении | При компиляции (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 / null | strictNullChecks, 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, когда нужна «номинальность» — Паттерны.
Если двум типам с одинаковой формой нужны разные имена в проверке, используют branded types или классы — см. Паттерны и Справочник — распространённые паттерны.
Проектирование типов и принцип надёжности
Хорошие типы в TS — это контракты, которые сложно нарушить случайно:
- Узкие union вместо
stringдля кодов состояния ("idle" | "loading" | "error"). - Discriminated union с полем
kind/type— исчерпывающийswitchиnever— 12.md, 28.md. - Разделение слоёв: 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 |
boolean | true / 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 Payment— type predicate: внутриif (isPayment(x))типx—Payment. - Отличие от
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 без серверного id | Omit<T, K> | Omit<User, "id"> |
кэш id → пользователь | Record<K, V> | Record<string, User> |
роли без admin | Exclude<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, подсказки, ошибки в реальном времени |
| Сборка / CI | tsc, tsc --noEmit | Весь граф проекта (или references в monorepo) |
| Runtime | JavaScript | Типов нет — только значения |
Аннотации : 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 type | Union не сужен | switch по дискриминанту, in, typeof |
Везде any ради быстрой сборки | Отключена проверка | unknown, типы API, рекомендации |
| Дублирование структуры | Нет общего type | interface / Pick / общий модуль types/ |
Практика
- Опишите
User,Admin,Guestс полем-дискриминантомkindи объедините вAccount. НапишитеgetDisplayName(account: Account)сswitch. - Смоделируйте загрузку API как
LoadState(idle / loading / success / error) и функциюrender(state)с exhaustivedefault. - Для объекта конфигурации используйте
satisfiesи убедитесь, что IDE подсказывает конкретные поля. - Возьмите JSON из
fetchкакunknownи напишитеparseUser(raw: unknown): Userс проверками (безany). - Сформируйте
PublicUserчерезOmitиUserPatchчерезPartial<Pick<…>>. - Опишите
ApiResponseиз трёх веток и типErrorResponse = Exclude<ApiResponse, OkResponse>; напишитеlogErrorс сужением поstatus. - Напишите
isUser(value: unknown): value is UserиassertUser(value: unknown): asserts value is User.
Смежные статьи
- Переменные и константы —
let/const,readonly, деструктуризация - Операторы и ветвления —
switchи discriminated unions - Функции — сигнатуры, overloads
- Дженерики —
Array<T>,Promise<T> - Объекты и классы — модификаторы, наследование
- Рекомендации — strict, валидация внешних данных
- Справочник раздела — маршрут по 301
- Обзор в контексте JS: Обзор TypeScript в JS