TypeScript — это JavaScript с описанием типов: компилятор tsc проверяет программу до запуска и находит опечатки в именах полей, забытые проверки на undefined и необработанные варианты. Сами типы при запуске стираются — выполняется обычный JavaScript. Для старта достаточно установить пакет typescript, включить strict в tsconfig.json и описать данные через type и interface.
После материала вы сможете
- описать данные через
typeиinterfaceс необязательными иreadonly-полями - сузить union-тип через
typeof, сравнение и поле-дискриминант и проверить полнотуswitchчерезnever - написать дженерик-функцию с ограничением
extendsиkeyof - принять JSON как
unknown, проверить его предикатами и настроитьtsconfig.jsonсоstrict
Проверено на
TypeScript 7.0.2TypeScript 6.0.3TypeScript 5.9.3Node.js 24 LTS (24.18.0)
Сверено с документацией: 11 октября 2026 г.
Содержание
Зачем типы: ошибка до запуска
Возьмём маленький модуль обработки заказов кофейни на JavaScript. Он считает сумму корзины, скидку по промокоду и подписывает статус заказа. В коде три ошибки — найдите их до того, как прочитаете вывод.
export function subtotal(order) {
return order.items.reduce((sum, item) => sum + item.price * item.quanity, 0);
}
export function discount(order) {
return Math.round(subtotal(order) * order.promo.percent / 100);
}
export function statusLabel(status) {
if (status === "new") return "Новый";
if (status === "paid") return "Оплачен";
if (status === "shipped") return "Отгружен";
}import { discount, statusLabel, subtotal } from "./orders.js";
const order = {
id: 1042,
status: "cancelled",
items: [{ sku: "CAP-M", name: "Капучино", price: 1600, qty: 2 }],
};
console.log(subtotal(order));
console.log(statusLabel(order.status));
console.log(discount(order));$ node js/main.js
NaN
undefined
…\js\orders.js:6
return Math.round(subtotal(order) * order.promo.percent / 100);
^
TypeError: Cannot read properties of undefined (reading 'percent')
at discount (…/js/orders.js:6:51)
at …
Node.js v24.18.0JavaScript промолчал дважды: опечатка quanity дала NaN, а для статуса "cancelled" функция вернула undefined. Только третья ошибка — заказ без промокода — уронила программу, и только в момент выполнения. В реальном проекте такие ошибки находит покупатель.
Документация TypeScript формулирует задачу так: TypeScript — статический анализатор типов для программ на JavaScript, инструмент, который запускается до вашего кода и проверяет, что типы в программе согласованы. Тип — это описание того, какие значения допустимы: число, строка, объект с определёнными полями, один из перечисленных вариантов.
Создайте проект
Нужен Node.js 24 LTS. Установите компилятор как зависимость разработки: тогда у всех участников проекта будет одна и та же версия.
Добавьте скрипты
checkзапускает проверку типов без сборки файлов,startвыполняет программу. Node.js 24 умеет запускать.ts-файлы сам, просто вырезая типы.Опишите правила проверки
Файл
tsconfig.jsonговорит компилятору, где лежит код и насколько строго его проверять. Подробно разберём его в последнем разделе.
mkdir orders-ts
cd orders-ts
npm init -y
npm install -D typescript
npx tsc -vnpx tsc -v на момент проверки выводит Version 7.0.2.{
"name": "orders-ts",
"private": true,
"type": "module",
"scripts": {
"check": "tsc --noEmit",
"start": "node src/main.ts"
},
"devDependencies": {
"typescript": "^7.0.2"
}
}{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"target": "es2024",
"module": "nodenext",
"noEmit": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"rewriteRelativeImportExtensions": true
},
"include": ["src"]
}Теперь переименуем orders.js в src/orders.ts и опишем, что функции принимают на вход. Типы Order и OrderStatus лежат в отдельном файле src/types.ts — его разберём в разделе о type и interface. Код функций не меняем.
import type { Order, OrderStatus } from "./types.ts";
export function subtotal(order: Order): number {
return order.items.reduce((sum, item) => sum + item.price * item.quanity, 0);
}
export function discount(order: Order): number {
return Math.round(subtotal(order) * order.promo.percent / 100);
}
export function statusLabel(status: OrderStatus): string {
if (status === "new") return "Новый";
if (status === "paid") return "Оплачен";
if (status === "shipped") return "Отгружен";
}$ npm run check
> check
> tsc --noEmit
src/orders.ts(4,68): error TS2339: Property 'quanity' does not exist on type 'OrderItem'.
src/orders.ts(8,39): error TS18048: 'order.promo' is possibly 'undefined'.
src/orders.ts(11,51): error TS2366: Function lacks ending return statement and return type does not include 'undefined'.- TS2339 — у
OrderItemнет поляquanity: опечатку видно сразу, безNaNв отчёте. - TS18048 —
promoнеобязательное поле, его может не быть, и обращаться кpercentбез проверки нельзя. - TS2366 — функция обещает вернуть
string, но для одного из статусов не возвращает ничего.
Базовые типы, вывод типов и аннотации
Аннотация типа — запись через двоеточие после имени: price: number. Но писать её везде не нужно. По документации, TypeScript везде, где может, выводит типы сам: тип переменной определяется по начальному значению. Handbook прямо советует новичкам писать меньше аннотаций, чем кажется нужным.
string
- Значения
- строки
- Пример
const name = "Капучино"
number
- Значения
- целые и дробные числа,
NaN - Пример
const price = 1600
boolean
- Значения
trueиfalse- Пример
let paid = false
string[] или Array<string>
- Значения
- массив строк
- Пример
const names = ["Капучино", "Чизкейк"]
null, undefined
- Значения
- «нет значения»
- Пример
promo?: Promo— поле может отсутствовать
any
- Значения
- что угодно, проверка отключена
- Пример
- избегаем, см. раздел про
unknown
let total = 0; // вывод типа: number
total = "6540 ₸";
const names = ["Капучино", "Чизкейк"]; // string[]
const count = names.length; // number
// у параметров аннотации нужны, тип результата (number) выведен сам
function lineTotal(price: number, qty: number) {
return price * qty;
}
// без аннотации параметр получил бы неявный any — strict это запрещает
function fee(order) {
return order.delivery.fee;
}$ npx tsc --noEmit
src/inference.ts(2,1): error TS2322: Type 'string' is not assignable to type 'number'.
src/inference.ts(13,14): error TS7006: Parameter 'order' implicitly has an 'any' type.Где аннотации нужны: у параметров функций. Компилятор не может угадать, что придёт в fee(order), и без режима strict молча присвоил бы параметру тип any — то есть отключил бы для него проверку. Флаг noImplicitAny, входящий в strict, превращает такую ситуацию в ошибку TS7006. Тип результата функции обычно выводится из return, поэтому у lineTotal его можно не писать.
type и interface: описываем данные
Все сущности модуля описаны в одном файле. interface задаёт форму объекта, type даёт имя любому типу — в том числе объединению вариантов.
export type OrderStatus = "new" | "paid" | "shipped" | "cancelled";
export interface OrderItem {
readonly sku: string;
name: string;
price: number; // тенге за штуку
qty: number;
}
export interface Promo {
code: string;
percent: number;
}
export type Delivery =
| { kind: "pickup" }
| { kind: "courier"; address: string; fee: number };
export interface Order {
readonly id: number;
customer: string;
status: OrderStatus;
items: OrderItem[];
promo?: Promo;
delivery: Delivery;
}promo?: Promo— необязательное поле. При чтении его типPromo | undefined, и перед использованием нужна проверка.readonly idиreadonly sku— поля, которые после создания объекта нельзя переприсвоить. По Handbook,readonlyне меняет поведение во время выполнения: это запрет только на этапе проверки.OrderStatus— объединение строковых литералов: статус может быть только одним из четырёх значений, а не любой строкой.Delivery— объединение двух форм объекта с общим полемkind. Такой тип разберём в следующем разделе.
import type { Order } from "./types.ts";
function renumber(order: Order) {
order.id = 1; // id объявлен как readonly
order.items[0].sku = "NEW"; // sku тоже readonly; и элемента [0] может не быть
order.promo.percent = 50; // promo необязателен
}$ npx tsc --noEmit
src/readonly.ts(4,9): error TS2540: Cannot assign to 'id' because it is a read-only property.
src/readonly.ts(5,3): error TS2532: Object is possibly 'undefined'.
src/readonly.ts(5,18): error TS2540: Cannot assign to 'sku' because it is a read-only property.
src/readonly.ts(6,3): error TS18048: 'order.promo' is possibly 'undefined'.Вторая строка дала сразу две ошибки. Кроме readonly сработал флаг noUncheckedIndexedAccess: элемент order.items[0] может не существовать, если корзина пустая, поэтому его тип — OrderItem | undefined. Обратите внимание: readonly защищает только само поле. Документация уточняет, что содержимое объекта в readonly-поле менять по-прежнему можно.
Форма объекта
interface- да
type- да
Объединение вариантов (A | B)
interface- нет
type- да
Расширение
interfaceinterface B extends Atype- пересечение
A & B
Дописать поля повторным объявлением
interface- да (declaration merging)
type- нет — повторное имя считается ошибкой
Handbook говорит, что в большинстве случаев выбор между ними — дело вкуса, и предлагает эвристику: использовать interface, пока не понадобятся возможности type. Мы так и сделали: объекты — интерфейсами, объединения — через type.
Union, литеральные типы и сужение
Union-тип (объединение) описывает значение, которое может быть одним из нескольких типов: Promo | undefined, "pickup" | "courier". По документации, с объединением разрешены только операции, допустимые для каждого его варианта. Чтобы работать с конкретным вариантом, тип нужно сузить — проверить в коде, какой вариант перед нами. Handbook называет это narrowing: уточнение типа до более конкретного, чем объявленный.
import type { Delivery, Order, OrderStatus } from "./types.ts";
export function subtotal(order: Order): number {
return order.items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
export function discount(order: Order): number {
if (order.promo === undefined) {
return 0;
}
// здесь order.promo уже имеет тип Promo, а не Promo | undefined
return Math.round((subtotal(order) * order.promo.percent) / 100);
}
export function deliveryFee(delivery: Delivery): number {
switch (delivery.kind) {
case "pickup":
return 0;
case "courier":
return delivery.fee; // fee есть только у курьерской доставки
}
}
export function orderTotal(order: Order): number {
return subtotal(order) - discount(order) + deliveryFee(order.delivery);
}
export function statusLabel(status: OrderStatus): string {
switch (status) {
case "new":
return "Новый";
case "paid":
return "Оплачен";
case "shipped":
return "Отгружен";
case "cancelled":
return "Отменён";
default: {
// сюда можно попасть, только если в OrderStatus появится новый статус
const unhandled: never = status;
return unhandled;
}
}
}discount: послеif (order.promo === undefined) return 0компилятор знает, что дальшеorder.promo— этоPromo. Так работает сужение через сравнение.deliveryFee: у обоих вариантовDeliveryесть полеkindс литеральным типом. Документация называет такой тип размеченным объединением (discriminated union): проверкаkindвswitchсужает весь объект, и в ветке"courier"доступно полеfee.statusLabel: веткаdefaultприсваивает статус переменной типаnever— «значений не бывает». Если все варианты обработаны, сюда ничего не доходит. Если вOrderStatusдобавят новый статус, присваивание станет ошибкой.
Проверим последний пункт: добавим в OrderStatus статус "refunded" и запустим проверку, не трогая statusLabel.
$ npx tsc --noEmit
src/orders.ts(40,13): error TS2322: Type '"refunded"' is not assignable to type 'never'.Компилятор показал ровно то место, которое нужно дописать. Без такой проверки функция вернула бы undefined, как в исходном JavaScript. Это приём из Handbook — exhaustiveness checking, проверка полноты.
typeof
- Пример
typeof value === "string"- Что узнаёт компилятор
- примитивный тип значения
Сравнение
- Пример
order.promo === undefined- Что узнаёт компилятор
- значение есть или его нет
Поле-дискриминант
- Пример
switch (delivery.kind)- Что узнаёт компилятор
- конкретный вариант объединения
Оператор in
- Пример
"total" in data- Что узнаёт компилятор
- у объекта есть такое поле
Array.isArray, instanceof
- Пример
error instanceof Error- Что узнаёт компилятор
- массив или экземпляр класса
Предикат типа
- Пример
function isItem(v: unknown): v is OrderItem- Что узнаёт компилятор
- то, что гарантирует ваша функция-проверка
Дженерики: одна функция для разных типов
Иногда функция одинаково работает с данными любой формы: взять первый элемент, найти запись по id, собрать значения одного поля. Если объявить параметр как any[], функция примет что угодно, но, как объясняет Handbook, потеряет информацию о типе: на выходе тоже будет any. Дженерик (generic) решает это через параметр типа — переменную, которая хранит не значение, а тип.
// T — параметр типа: «какой массив пришёл, элемент такого же типа и вернётся»
export function first<T>(items: readonly T[]): T | undefined {
return items[0];
}
// T обязан иметь числовое поле id — это ограничение (constraint)
export function findById<T extends { id: number }>(
items: readonly T[],
id: number,
): T | undefined {
return items.find((item) => item.id === id);
}
// K — только существующие ключи T, а результат — массив значений этого поля
export function pluck<T, K extends keyof T>(items: readonly T[], key: K): T[K][] {
return items.map((item) => item[key]);
}first<T>—Tподставляется при вызове автоматически. Дляfirst(order.items)компилятор выведетT = OrderItem, а результат будетOrderItem | undefined: массив может быть пустым.findById<T extends { id: number }>— ограничение (constraint): подойдёт любой массив объектов, у которых есть числовоеid, и вернётся объект того же типа, а не урезанный{ id: number }.pluck<T, K extends keyof T>—keyof Tозначает «имя одного из полейT». РезультатT[K][]— массив значений именно этого поля: для"price"получитсяnumber[], для"name"—string[].
import type { Order } from "./types.ts";
import { first, pluck } from "./utils.ts";
function report(order: Order) {
const prices = pluck(order.items, "price"); // number[]
const titles = pluck(order.items, "title"); // такого поля нет
const top = first(order.items);
console.log(top.name); // first может вернуть undefined
}$ npx tsc --noEmit
src/generic-error.ts(6,37): error TS2345: Argument of type '"title"' is not assignable to parameter of type 'keyof OrderItem'.
src/generic-error.ts(8,15): error TS18048: 'top' is possibly 'undefined'.По документации, обычно параметр типа не указывают явно: компилятор выводит его из аргументов. Явная запись вида first<OrderItem>(items) нужна, когда вывести тип не получается. Если дженерик-функцию трудно прочитать, это сигнал упростить её: параметр типа нужен там, где он связывает вход и выход.
unknown против any: типизируем JSON с сервера
TypeScript проверяет только ваш код. Данные, которые приходят по сети, из localStorage или от пользователя, компилятор увидеть не может. JSON.parse возвращает any, и это опасно: по Handbook, any отключает проверку типов, и дальше любая опечатка проходит молча. Безопасная альтернатива — unknown: документация описывает его как тип для любого значения, с которым ничего нельзя делать, пока вы не докажете, что это за значение.
const fromAny: any = JSON.parse(`{ "total": 6540 }`);
fromAny.totl.toFixed(); // компилятор молчит — упадёт при запуске
const fromUnknown: unknown = JSON.parse(`{ "total": 6540 }`);
fromUnknown.total; // ошибка: сначала докажите, что это объект с полем total
if (typeof fromUnknown === "object" && fromUnknown !== null && "total" in fromUnknown) {
console.log(fromUnknown.total); // тип поля здесь — unknown, его тоже нужно проверить
}any пропускает опечатку totl, unknown — нет.$ npx tsc --noEmit
src/unknown.ts(5,1): error TS18046: 'fromUnknown' is of type 'unknown'.Чтобы превратить unknown в Order, нужны проверки во время выполнения. Функции с возвращаемым типом value is OrderItem — предикаты типа: если функция вернула true, компилятор сужает значение до OrderItem.
import type { Delivery, Order, OrderItem, OrderStatus, Promo } from "./types.ts";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function isStatus(value: unknown): value is OrderStatus {
return value === "new" || value === "paid" || value === "shipped" || value === "cancelled";
}
function isItem(value: unknown): value is OrderItem {
return (
isRecord(value) &&
typeof value.sku === "string" &&
typeof value.name === "string" &&
typeof value.price === "number" &&
typeof value.qty === "number"
);
}
function isPromo(value: unknown): value is Promo {
return isRecord(value) && typeof value.code === "string" && typeof value.percent === "number";
}
function isDelivery(value: unknown): value is Delivery {
if (!isRecord(value)) return false;
if (value.kind === "pickup") return true;
return (
value.kind === "courier" &&
typeof value.address === "string" &&
typeof value.fee === "number"
);
}
export function parseOrder(json: string): Order {
const data: unknown = JSON.parse(json);
if (
!isRecord(data) ||
typeof data.id !== "number" ||
typeof data.customer !== "string" ||
!isStatus(data.status) ||
!Array.isArray(data.items) ||
!data.items.every(isItem) ||
!isDelivery(data.delivery)
) {
throw new Error("Сервер вернул заказ в неожиданном формате");
}
const order: Order = {
id: data.id,
customer: data.customer,
status: data.status,
items: data.items,
delivery: data.delivery,
};
if (isPromo(data.promo)) {
order.promo = data.promo;
}
return order;
}import { orderTotal, statusLabel } from "./orders.ts";
import { parseOrder } from "./parse.ts";
import { findById, first, pluck } from "./utils.ts";
// так мог бы выглядеть ответ сервера — пока это просто строка
const response = `{
"id": 1042,
"customer": "Алия",
"status": "paid",
"items": [
{ "sku": "CAP-M", "name": "Капучино", "price": 1600, "qty": 2 },
{ "sku": "CHS-1", "name": "Чизкейк", "price": 2400, "qty": 1 }
],
"promo": { "code": "AUTUMN", "percent": 10 },
"delivery": { "kind": "courier", "address": "ул. Абая, 10", "fee": 1500 }
}`;
const order = parseOrder(response);
console.log(`Заказ ${order.id}: ${statusLabel(order.status)}`);
console.log(pluck(order.items, "name").join(", "));
console.log(`К оплате: ${orderTotal(order)} ₸`);
const top = first(order.items); // тип: OrderItem | undefined
console.log(top?.name ?? "корзина пуста");
const found = findById([order], 1042); // тип: Order | undefined
console.log(found ? "нашли заказ" : "нет такого заказа");
try {
parseOrder(`{ "id": "1042" }`);
} catch (error) {
console.log(error instanceof Error ? error.message : error);
}$ npm run check && npm start
> check
> tsc --noEmit
> start
> node src/main.ts
Заказ 1042: Оплачен
Капучино, Чизкейк
К оплате: 6540 ₸
Капучино
нашли заказ
Сервер вернул заказ в неожиданном форматеИнженерный компромисс: ручные проверки, как в parse.ts, хороши для учебного примера и пары эндпоинтов. В проектах с десятками ответов API обычно используют библиотеку описания схем (например, Zod), где тип и проверка выводятся из одного описания и не расходятся.
tsconfig и strict: минимальная настройка
tsconfig.json хранит настройки компилятора, чтобы не передавать их флагами. Наш файл короткий, и каждая строка в нём нужна.
strict: true
- Что делает
- Включает семейство строгих проверок, среди них
noImplicitAnyиstrictNullChecks. Документация рекомендуетstrictNullChecksвезде, где это практически возможно.
noUncheckedIndexedAccess
- Что делает
- Добавляет
undefinedк типу элемента при доступе по индексу:items[0]— этоOrderItem | undefined. Вstrictне входит.
target: es2024
- Что делает
- Какой версии JavaScript соответствует результат и какие встроенные API считаются доступными.
module: nodenext
- Что делает
- Правила модулей как в Node.js: с учётом
"type": "module"и с расширениями в путях импорта.
noEmit
- Что делает
- Только проверять, не создавать
.js-файлы: код выполняет сам Node.js.
erasableSyntaxOnly
- Что делает
- Запрещает конструкции TypeScript, которые нельзя просто вырезать (
enum,namespaceс кодом, parameter properties): Node.js их не выполнит.
verbatimModuleSyntax
- Что делает
- Требует
import typeдля импорта одних только типов, чтобы при вырезании типов не оставались лишние импорты.
rewriteRelativeImportExtensions
- Что делает
- Разрешает импорты с расширением
.ts— Node.js требует указывать расширение файла.
Что важно знать о версиях. TypeScript 7.0 — новый компилятор, переписанный на Go: по объявлению команды, полная сборка ускоряется примерно в 8–12 раз, а проверка типов совместима с 6.0. Однако программного API у 7.0 пока нет, поэтому инструменты, которые встраивают TypeScript (например, typescript-eslint), работают через совместимый пакет 6.0, а шаблоны вроде Vite пока ставят TypeScript 6.0. Для этого урока разницы нет: синтаксис и сообщения об ошибках одинаковы.
Node.js 24 запускает .ts-файлы напрямую: по документации Node.js, type stripping (вырезание типов) стабилен с версии 24.12 и включён по умолчанию. Но Node.js не проверяет типы и не читает tsconfig.json — поэтому npm run check перед запуском обязателен. В браузерных проектах ту же роль играет сборщик, например Vite, а проверку делает tsc.
Практика и следующий шаг
Итог урока: типы описывают данные, сужение помогает компилятору понять, какой вариант перед ним, дженерики сохраняют типы в переиспользуемых функциях, а unknown и проверки защищают границу с внешним миром. Дальше по траектории React — компоненты и props: те же интерфейсы будут описывать, какие данные принимает каждый кусок интерфейса.
Как проверено6 · пройденосверено 11 октября 2026 г.
- запущено, работает
js/orders.js + js/main.js (исходный JS с тремя ошибками)
node js/main.js на Node.js 24.18.0: вывод NaN, undefined, TypeError — приведён в статье (путь сокращён)
- запущено, работает
src/orders.ts — первая версия (before/orders.ts) и вывод npm run check
tsc 7.0.2 --noEmit в temp-проекте: ровно три ошибки TS2339, TS18048, TS2366 — вывод скопирован
- запущено, работает
src/types.ts, src/orders.ts, src/utils.ts, src/parse.ts, src/main.ts, package.json, tsconfig.json
npm run check (tsc 7.0.2, strict + noUncheckedIndexedAccess) — 0 ошибок; npm start (node src/main.ts, type stripping) — вывод совпадает со статьёй; дополнительно tsc --noEmit на 6.0.3 и 5.9.3 — 0 ошибок
- запущено, работает
Учебные файлы с намеренными ошибками: inference.ts, readonly.ts, generic-error.ts, unknown.ts; добавление "refunded" (never)
каждый файл по очереди помещён в src и проверен tsc 7.0.2; ошибки в статье — дословный вывод
- запущено, работает
Решения упражнений (refunded, locker, sumBy/itemsCount)
применены к копии проекта (ex/), tsc --noEmit — 0 ошибок, node src/main.ts — работает; фрагменты в статье сверены со скомпилированными файлами программно
- запущено, работает
EN-версия кода (переведённые строки и комментарии)
отдельная копия проекта en/ и en/ex/: тот же набор проверок, вывод ошибок совпадает по строкам и столбцам
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.