Объединения (union)
Переменная может принимать значения нескольких типов. Знак
| читается как «или»:
function printId(id: number | string): void {
console.log(`ID: ${id}`);
}
printId(42); // OK
printId("abc"); // OK
// printId(true); // Ошибка
Литеральные типы
Когда значение ограничено набором конкретных строк или чисел:
type Status = "new" | "in_progress" | "done";
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
function setStatus(s: Status): void { /* ... */ }
setStatus("new");
// setStatus("closed"); // Ошибка: "closed" нет в объединении
as const — превращение массива в литеральный тип
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = typeof ROLES[number]; // "admin" | "editor" | "viewer"
function checkAccess(role: Role): boolean {
return role === "admin";
}
Narrowing (сужение типа)
TypeScript сужает тип внутри условных блоков. Это называется narrowing — основа безопасной работы с объединениями.
Type guards через typeof
function process(value: number | string): number {
if (typeof value === "number") {
return value * 2; // здесь value: number
}
return value.length; // здесь value: string
}
instanceof для классов
class ValidationError extends Error {}
class NetworkError extends Error {}
function handle(err: Error): void {
if (err instanceof ValidationError) {
console.log("Проверка ввода:", err.message);
} else if (err instanceof NetworkError) {
console.log("Сеть:", err.message);
}
}
in для проверки свойства
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
}
}
Это дискриминируемое объединение — приём с общим полем
(kind), которое определяет форму объекта. Идеально для конечных
автоматов, событий, состояний.
Пользовательские type guards
Когда проверка сложнее typeof, описывают функцию-предикат с
возвращаемым типом value is Type:
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird): void {
if (isFish(pet)) {
pet.swim(); // TS знает: pet — Fish
} else {
pet.fly();
}
}
Assertion functions
Функция, которая бросает ошибку, если условие не выполнено. Помечается
возвращаемым типом asserts condition:
function assertDefined<T>(value: T | undefined | null): asserts value is T {
if (value === undefined || value === null) {
throw new Error(`Ожидалось значение, получено ${value}`);
}
}
const user = getUser();
assertDefined(user);
console.log(user.name); // TS знает: user — User, а не User | undefined
Это удобно для валидации входных данных в начале функции — дальше код пишется без лишних проверок.
Utility-типы
TypeScript поставляется с набором встроенных утилит для преобразования типов. Самые ходовые:
Pick — выбрать поля
interface User {
id: number;
name: string;
email: string;
passwordHash: string;
}
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }
Omit — исключить поля
type UserPublic = Omit<User, "passwordHash">;
// { id: number; name: string; email: string }
Partial — все поля необязательными
type UserUpdate = Partial<User>;
// все поля необязательны — удобно для PATCH-запросов
Required — наоборот, все обязательны
interface Article {
title: string;
subtitle?: string;
}
type StrictArticle = Required<Article>; // subtitle становится обязательным
Readonly — неизменяемый объект
type FrozenUser = Readonly<User>;
const u: FrozenUser = { ... };
// u.name = "x"; // Ошибка
Record — словарь
type UsersById = Record<number, User>;
const users: UsersById = {
1: { id: 1, name: "Анна", email: "a@x.ru", passwordHash: "..." },
};
ReturnType — тип возвращаемого значения функции
function getUser() {
return { id: 1, name: "Анна" };
}
type User = ReturnType<typeof getUser>;
// { id: number; name: string }
Parameters — тип параметров функции
type Args = Parameters<typeof getUser>;
// []
Паттерн: DTO и модель
Часто форма данных «на вход» отличается от «внутри системы». Utility-типы помогают не дублировать описания:
interface User {
id: number;
name: string;
email: string;
createdAt: string;
}
// То, что присылает клиент при создании (без id и createdAt)
type CreateUserDTO = Omit<User, "id" | "createdAt">;
// То, что присылает клиент при обновлении (все поля необязательны)
type UpdateUserDTO = Partial<CreateUserDTO>;
// Что возвращаем по API (без внутреннего поля)
type UserResponse = Omit<User, "email">;
function createUser(dto: CreateUserDTO): User {
return {
...dto,
id: nextId(),
createdAt: new Date().toISOString(),
};
}
Типизация данных «снаружи»
Главная опасность: TypeScript не проверяет данные из API, localStorage, JSON-файлов. Аннотация — это обещание, а не гарантия:
// ОПАСНО: реальный объект может быть любым
const user: User = await fetch("/api/me").then((r) => r.json());
user.name; // может упасть в рантайме, если name нет
Валидация через zod
Библиотека zod позволяет описать схему и получить из неё
TypeScript-тип + проверить данные в рантайме:
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>; // тип выводится из схемы
async function fetchUser(): Promise<User> {
const raw = await fetch("/api/me").then((r) => r.json());
return UserSchema.parse(raw); // проверит и бросит ошибку, если не подходит
}
Это золотой стандарт работы с внешними данными в современных проектах.
Паттерн Result для async-операций
Вместо try/catch удобно возвращать тип-результат — успех или
ошибку. Особенно полезно для обработки ошибок сети и валидации:
type Result<T, E = string> =
| { ok: true; value: T }
| { ok: false; error: E };
async function fetchUser(id: number): Promise<Result<User>> {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
return { ok: false, error: `HTTP ${res.status}` };
}
const value = await res.json();
return { ok: true, value };
} catch (e) {
return { ok: false, error: e instanceof Error ? e.message : "unknown" };
}
}
const result = await fetchUser(1);
if (result.ok) {
console.log(result.value.name); // TS знает, что value есть
} else {
console.log(result.error);
}
Типобезопасная конфигурация
Реальный приём — конфиг с проверкой во время запуска приложения. Так ошибка в переменной окружения ловится сразу, а не через час в продакшене:
import { z } from "zod";
const EnvSchema = z.object({
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
});
type Env = z.infer<typeof EnvSchema>;
// Парсится один раз при старте — если что-то не так, приложение не запустится
const env: Env = EnvSchema.parse(process.env);
console.log(env.DATABASE_URL);
console.log(env.PORT); // точно number
Советы для крупных проектов
- Включайте
strictс первого дня — потом включать больно. - Избегайте
any, используйтеunknownдля внешних данных. - Валидируйте данные на границе системы (API, localStorage, файлы) —
zodили аналоги. - Не дублируйте типы — используйте
Pick,Omit,Partialдля производных типов. - Разделяйте DTO и доменные модели — то, что присылает клиент, не обязано совпадать с тем, что хранится в БД.
- Дискриминируемые объединения для событий и состояний — это делает
switchисчерпывающим и безопасным. - Project references для монорепозиториев и крупных проектов.
- Запускайте
tsc --noEmitв CI — отдельная проверка типов без сборки.
Типичные ошибки
1. as вместо валидации
// Плохо: говорим TypeScript "доверься"
const user = JSON.parse(raw) as User;
// Хорошо: проверяем схему
const user = UserSchema.parse(JSON.parse(raw));
2. Необработанный null/undefined
const el = document.querySelector(".btn");
el.addEventListener("click", () => {});
// ^^^ Ошибка: возможно, el — null
// Проверка
if (el) {
el.addEventListener("click", () => {});
}
// Или сразу после запроса — assertion function
assertDefined(el);
el.addEventListener("click", () => {});
3. switch без default у дискриминируемого объединения
Если добавили новое значение в объединение и забыли обработать — TypeScript
с подсветкой неисчерпывающего switch не даст этого пропустить (через
never):
type Shape = { kind: "circle" } | { kind: "square" };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return 1;
case "square": return 2;
default:
// Если добавим { kind: "triangle" }, TS укажет здесь на ошибку
const _exhaustive: never = s;
return _exhaustive;
}
}
Практика
- Опишите тип
Status = "new" | "paid" | "done"и функцию, возвращающую описание для каждого значения. - Создайте дискриминируемое объединение
Notification(email, sms, push) и функцию отправки сswitch. - Реализуйте тип
Result<T>и функцию-обёртку дляfetch, возвращающуюResult. - Подключите
zodи опишите схему для объекта пользователя; получите тип черезz.infer. - Используя
Pick,Omit,Partial, постройте из интерфейсаUserтри производных типа: для создания, обновления и публичного представления.
Итог курса
В финальной главе мы собрали вместе всё изученное: объединения и
литеральные типы, сужение через typeof/instanceof/in,
пользовательские type guards и assertion functions, utility-типы и паттерны
для реальных проектов — DTO, валидация через zod, тип Result и
типобезопасная конфигурация.
Поздравляем! Вы прошли базовый курс TypeScript. Дальше стоит:
- Освоить декораторы (метаданные классов, фреймворки NestJS / TypeORM).
- Изучить продвинутые типы:
infer, mapped types, conditional types, template literal types. - Попробовать TypeScript в реальном фреймворке: React, Vue, NestJS.
- Настроить
ESLintс правилами для TypeScript и интегрировать в CI.
Главное — пишите код. TypeScript раскрывается полностью только в реальных проектах, когда типы начинают ловить ошибки, которые раньше отнимали часы отладки.
Комментарии 0
Пока нет комментариев. Станьте первым!