TypeScript развивается быстрее, чем успевает большинство экосистем. Каждая версия приносит новый синтаксис, более строгие проверки и шаблоны, заново определяющие, что считать идиоматичным. Разница между кодом, который просто компилируется, и кодом, который действительно пользуется системой типов, огромна — и именно она решает, будут ваши типы документацией или шумом.
Эта статья о шаблонах, которые важнее всего для продакшен-кода на TypeScript в 2026 году. Это не академические упражнения, а практики, которые делают большие кодовые базы безопаснее, API труднее использовать неправильно, а рефакторинг — менее пугающим. Каждый пример взят из реальных шаблонов, применяемых в системах с миллионами запросов.
Фундамент: конфигурация TypeScript на 2026 год
Самое весомое решение о качестве TypeScript лежит не в коде, а в tsconfig.json. Планка ушла дальше strict: true. В 2026 году продакшен-конфигурация должна включать проверки, которые раньше были опциональными или экспериментальными.
// tsconfig.json - the 2026 baseline
{
"compilerOptions": {
"strict": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"noPropertyAccessFromIndexSignature": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}Каждый флаг убирает свой класс ошибок. exactOptionalPropertyTypes не даёт совершить частую ошибку, когда необязательному свойству явно присваивают undefined: различие между «отсутствует» и «есть, но undefined» важно на границах API. noUncheckedIndexedAccess заставляет обрабатывать undefined при любом обращении по динамическому ключу и ловит падения ещё до выкладки. verbatimModuleSyntax приводит разрешение модулей в соответствие со средой выполнения и устраняет тихие расхождения, которые ломают проекты на ESM в продакшене.
Команды, принявшие такую конфигурацию, сообщают о заметном снижении числа инцидентов, связанных с пустыми ссылками и неопределёнными свойствами. Усилия на обработку дополнительных проверок undefined ничтожны по сравнению с разбором падения из-за того, что в ответе API не оказалось поля, которое вы считали обязательным.
Размеченные объединения — ваш самый сильный шаблон
Размеченные объединения — самый результативный шаблон в TypeScript. Они моделируют состояния явно, делают недопустимые состояния непредставимыми и дают компилятору информацию, необходимую для исчерпывающей обработки. Если вы не используете их для данных, принимающих несколько форм, вы боретесь с системой типов вместо того, чтобы заставить её работать на себя.
type ApiState<S, E = Error> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: S }
| { status: "error"; error: E };
// Exhaustive match - if you add a status, this breaks at compile time
function renderState<S>(state: ApiState<S>): string {
switch (state.status) {
case "idle":
return "Awaiting input";
case "loading":
return "Loading...";
case "success":
return `Got ${JSON.stringify(state.data)}`;
case "error":
return `Failed: ${state.error.message}`;
}
}
// Usage - impossible to access data on a loading state
const userState: ApiState<User> = { status: "loading" };
// userState.data - does not compileВся сила в сужении типов. Когда вы проверяете state.status === 'success', TypeScript точно знает, в какой вы ветке, и подставляет корректный тип для каждого поля. Это убирает целые категории защитных проверок, которые иначе понадобились бы во время выполнения. Компилятор становится вашим набором тестов на недопустимые переходы состояний.
Чтобы шаблон работал, свойство-дискриминант (в примере выше это status) должно быть литеральным типом, а не обычной строкой. У каждого варианта должно быть своё уникальное литеральное значение. По нему компилятор различает ветки и предупредит, если у двух вариантов дискриминант совпадёт.
- Всегда берите строковый или числовой литерал в качестве дискриминанта — никогда обобщённый строковый тип.
- Держите общие свойства минимальными. Всё, что различается между вариантами, должно лежать в самом варианте.
- Сочетайте с типом never для проверки полноты: объявите переменную типа never в ветке default внутри switch, чтобы ловить необработанные случаи на этапе компиляции.
Шаблонные литеральные типы и «помеченные» типы
Шаблонные литеральные типы и помеченные типы решают две разные задачи, которые обычным типам не по силам. Первые дают проверку строк на уровне типов. Вторые приносят номинальную типизацию в структурно типизированный мир: они позволяют различать значения одинаковой формы, но разного смысла.
// Template literal type - valid API routes are checked at compile time
type ApiRoute = `/api/${string}`;
type UserRoute = `/api/users/${string}`;
function fetchApi<T>(route: ApiRoute): Promise<T> {
return fetch(route).then((r) => r.json());
}
// fetchApi("/invalid"); // Error: not assignable
// fetchApi("/api/users/123"); // OK
// Branded type - distinguish IDs that are both strings
type UserId = string & { __brand: "UserId" };
type OrderId = string & { __brand: "OrderId" };
function getUser(id: UserId): Promise<User> {
return db.users.find(id);
}
const orderId = "ord_123" as OrderId;
// getUser(orderId); // Error: Type 'OrderId' is not assignable to type 'UserId'Шаблонные литеральные типы блистают там, где строки следуют предсказуемому формату: построители маршрутов API, генераторы классов CSS, сопоставление ключей локализации, системы имён событий. Синтаксис интуитивен: вы пишете шаблон с подстановками ${}, а TypeScript проверяет, что реальные значения ему соответствуют.
Помеченные типы решают другую задачу. TypeScript типизирован структурно: два типа одинаковой формы взаимозаменяемы. Обычно это удобно, но становится опасным, когда у вас есть идентификаторы, представляющие разные сущности и при этом являющиеся строками. UserId и OrderId не должны подменять друг друга. Пересечение с { __brand: 'X' } создаёт фантомный тип, существующий только на этапе компиляции, — во время выполнения он не стоит ничего.
Помеченные типы — самое близкое к номинальной типизации, что TypeScript даёт без дополнительных инструментов. Одно пересечение с фантомным свойством предотвращает целый класс ошибок, когда не тот идентификатор уходит не в ту функцию.
Оператор satisfies — вывод типов без жертв
До появления satisfies разработчики стояли перед дилеммой, определяя константы, которые должны соответствовать типу, но сохранять свои литеральные значения. Можно было либо аннотировать тип и потерять точный вывод, либо опустить аннотацию и потерять проверку. Оператор satisfies снимает этот размен полностью.
type ColorPalette = {
primary: string;
secondary: string;
accent: string;
};
// Before satisfies - loses literal types
const paletteOld: ColorPalette = {
primary: "#0f0f0f", // type is string, not "#0f0f0f"
secondary: "#ffffff",
accent: "#0055ff",
};
// After satisfies - validates shape, keeps literals
const palette = {
primary: "#0f0f0f", // type is "#0f0f0f"
secondary: "#ffffff",
accent: "#0055ff",
} satisfies ColorPalette;
// palette.primary; // type is "#0f0f0f", not stringОператор satisfies особенно ценен в объектах конфигурации, обработчиках событий и структурах соответствия, где вам нужна типобезопасность формы, но при этом максимально узкие типы значений. Он заменяет целое созвездие обходных путей — as const вместе с аннотациями типа, избыточные приведения, отдельные функции проверки — одним ключевым словом.
Обобщённые шаблоны и типобезопасные клиенты API
Обобщения в TypeScript просты в базовых случаях — Array<T>, Promise<T>, — но по-настоящему сильными становятся, когда вы сочетаете ограничения, условные типы и вывод в одной сигнатуре. Самое практичное применение продвинутых обобщений — построение типобезопасных клиентов API, которые убирают целые категории ошибок времени выполнения.
// Type-safe API client
import { z } from "zod";
// Infer the output type from a Zod schema
type InferSchema<T extends z.ZodTypeAny> = T["_output"];
class ApiClient {
constructor(private base: string) {}
get<TSchema extends z.ZodTypeAny>(
path: string,
schema: TSchema,
params?: Record<string, string>
): Promise<InferSchema<TSchema>> {
const resolved = params
? Object.entries(params).reduce(
(p, [k, v]) => p.replace(`:${k}`, v),
path
)
: path;
return fetch(`${this.base}${resolved}`)
.then((r) => r.json())
.then((d) => schema.parse(d) as InferSchema<TSchema>);
}
}
const api = new ApiClient("https://api.example.com");
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
// api.get("/users/:id", userSchema, { id: "123" });
// Result type: { id: string; name: string; email: string }Этот шаблон сочетает несколько продвинутых приёмов. Условные типы с infer определяют, содержит ли маршрут параметры пути, и в зависимости от этого делают аргумент обязательным. Схема ответа разбирается во время выполнения и выводится на этапе компиляции, поэтому возвращаемый тип всегда корректен. Если API меняет структуру ответа, вы правите схему — и компилятор находит каждого потребителя, который из-за этого ломается.
Шаблон масштабируется на сотни эндпоинтов почти без усилий. Каждый эндпоинт — это строка пути и схема. Обобщённая инфраструктура делает остальное: подстановку параметров, проверку ответа и вывод типов. Команды, применяющие этот подход, сообщают об устранении большей части ошибок интеграции между фронтендом и бэкендом.
- Сочетайте схемы Zod или ArkType с обобщёнными обёртками над fetch, чтобы типобезопасность шла сквозь границу сети.
- Используйте условные типы с infer, чтобы делать аргументы обязательными или необязательными в зависимости от структуры маршрута.
- Возвращайте из клиента помеченные типы, чтобы вызывающая сторона не могла случайно перепутать идентификаторы разных сущностей.
Шаблоны обработки ошибок и расширение модулей
Обработка ошибок — та область, где большинство кодовых баз на TypeScript скатывается к any и делает вид, что проблемы нет. Шаблон Result, пришедший из Rust и функционального программирования, вводит ошибки в систему типов, и компилятор начинает требовать обработки каждого пути ошибки. Расширение модулей распространяет тот же подход на сторонние типы и позволяет добавить типобезопасность библиотекам, чьи определения неполны или слишком свободны.
// Result type - errors are part of the return type, not thrown
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function fetchUser(id: UserId): Promise<Result<User, ApiError>> {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
return { ok: false, error: await ApiError.fromResponse(res) };
}
return { ok: true, value: await res.json() };
} catch (err) {
return { ok: false, error: new ApiError("network", String(err)) };
}
}
// Consumer must handle both branches - compiler enforces it
const result = await fetchUser(userId);
if (result.ok) {
console.log(result.value.name); // result.value is User
} else {
console.error(result.error.code); // result.error is ApiError
}
// --- Module augmentation for third-party types ---
declare module "express-session" {
interface SessionData {
userId?: string;
role: "admin" | "user" | "viewer";
permissions: string[];
}
}Шаблон Result требует явной обработки ошибки в каждой точке вызова. Тихо проигнорировать неудачную операцию невозможно: компилятор требует проверить result.ok прежде, чем обращаться к result.value. Это убирает забытый try-catch — источник бесчисленных инцидентов. Платой становится чуть больше кода на каждой точке вызова, зато ни одна ошибка не остаётся необработанной.
Расширение модулей закрывает пробелы там, где сторонние определения типов недостаточны. Многие известные библиотеки поставляются со слишком свободными типами: функции возвращают any, параметры объявлены как object, в интерфейсах не хватает свойств. Вместо приведения через as или @ts-ignore объявите declare module 'имя-библиотеки' в файле .d.ts или .ts и допишите недостающие типы. Объявления сливаются автоматически, и вся кодовая база получает исправление.
Сочетание этих шаблонов — строгая конфигурация, размеченные объединения, шаблонные литеральные типы, помеченные типы, satisfies, типобезопасные обобщения и явная обработка ошибок — складывается в целостный подход к TypeScript в 2026 году. Каждый шаблон полезен сам по себе, но вместе они дают кодовую базу, где компилятор ловит ошибки, которые иначе стали бы инцидентами. Вложение в их освоение многократно окупается сокращением времени на отладку, более безопасным рефакторингом и API, которые действительно трудно использовать неправильно.