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

Управление типами

Цель

Типизация - обязательный механизм, обеспечивающий предсказуемость, безопасность рефакторинга и согласованность с API.
Типы используются как контракт, а не как “подсказка для редактора”.

Типы должны:

  • описывать реальные данные, а не “желаемое поведение”
  • синхронизироваться с API
  • предотвращать расхождения между ожидаемыми и фактическими структурами данных
  • упрощать навигацию и понимание предметной области проекта

Общие правила

MUST

  • TypeScript используется в strict mode.
  • Любые данные, полученные извне модуля (даже в пределах одного репозитория), считаются недоверенными.
  • Типизация не должна “маскировать” потенциально некорректные данные.
  • Отсутствие уверенного типа (unknown) предпочтительнее некорректного типа.

FORBIDDEN

  • any
  • unknown без последующей проверки (assert / type guard / schema validation)
  • type casting (as) для обхода ошибок компилятора

    Допускается только в редких случаях, локально и с техническим обоснованием. Предпочтительнее - type predicates / guards.

  • "типизация на глаз", не отражающая реальную структуру данных

Любая настройка генерации/типизационного процесса требует согласования с Tech Lead и PM.


Описание подходов к типизации

1) OpenAPI генерация DTO (MUST, если OpenAPI актуальна)

Принцип

Если у бэкенда есть OpenAPI (Swagger) и она поддерживается актуальной, DTO типы обязаны генерироваться автоматически.

Плюсы

  • максимальная синхронизация FE/BE
  • меньше ручных ошибок и риска рассинхрона
  • быстрый рефакторинг при изменениях API благодаря static typing

Минусы

  • нужно поддерживать механизм генерации
  • возможны неудобные названия/структуры типов, решается маппингом (mapper layer)

Важное правило: generated types не коммитятся

MUST

  • Сгенерированные типы не должны коммититься в репозиторий проекта, чтобы:
    • не было “искушения” править их руками
    • типы всегда соответствовали источнику истины

Как обеспечиваем (пример политики)

✅ Хорошо

  • generated папка добавлена в .gitignore
  • генерация выполняется через scripts
  • CI проверяет, что типы можно сгенерировать и они консистентны

Пример.gitignore

# generated API types (OpenAPI/GraphQL)
src/shared/api/generated/
packages/api/generated/

Пример package.json scripts

{
"scripts": {
"api:generate": "openapi-typescript ./openapi.json -o src/shared/api/generated/index.ts",
"api:check": "pnpm api:generate && git diff --exit-code"
}
}

Вариант api:check полезен, если вы всё-таки коммитите generated (но по правилу выше - не надо). Если generated не коммитятся, то api:check заменяется на проверку наличия OpenAPI файла + успешной генерации в CI.

Инструменты

  • openapi-typescript
  • orval

2) Schema-first + runtime-валидация (если OpenAPI нет или не источник истины)

Принцип

Типы выводятся из runtime-схем (zod.infer<> / valibot) и данные валидируются на входе:

  • ответы внешних API
  • формы
  • небезопасные источники данных

Когда использовать

  • OpenAPI отсутствует или не гарантирует актуальность
  • много внешних источников данных
  • критично ловить некорректные данные в runtime, а не только в compile-time

Плюсы

  • типы и runtime-проверки из одного источника (schema)
  • меньше случаев, когда TypeScript “верит, а данные сломаны”
  • отлично подходит для форм и внешних API

Минусы

  • ручная поддержка схем
  • нужна дисциплина: валидировать данные на входе

Инструменты

  • Zod
  • Valibot

✅ Пример (Zod)

import { z } from 'zod';

export const UserDtoSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email().optional()
});

export type UserDto = z.infer<typeof UserDtoSchema>;

export function parseUserDto(input: unknown): UserDto {
return UserDtoSchema.parse(input);
}

3) Комбинированный подход (OpenAPI + runtime-валидация)

Когда использовать

  • есть OpenAPI (types generated)
  • но нужна runtime-валидация на входе (формы, внешние данные, критичные payload)

Допускается

  • генерация DTO из OpenAPI
  • runtime-схемы для валидации на входе

MUST

  • runtime-схемы не заменяют OpenAPI, а дополняют её
  • источник истины для DTO — OpenAPI, а runtime-схемы — для защиты на границах системы

4) Ручные DTO-типы (fallback, только по согласованию)

Принцип

DTO описываются вручную на фронтенде в соответствии со схемами API (если они есть).

Когда допускается

  • OpenAPI отсутствует или типы организованы неверно со стороны backend
  • API стабилен и редко меняется
  • проект небольшой

Плюсы

  • быстрый старт
  • полный контроль над неймингом и структурой типов

Минусы

  • высокий риск рассинхрона payload/response с backend API
  • больше багов при изменениях API

Обязательные условия (MUST)

  • структура DTO отражает API-модули
  • любые изменения API согласуются с фронтенд-разработчиком
  • подход согласуется с Tech Lead и PM (особенно если ранее использовались другие подходы)

Строгое разделение DTO и UI-типов

Принцип

Не смешивать типы API (DTO) с типами UI/клиентской логики.

Почему:

  • UI-компонентам часто нужны дополнительные поля (например isLoading, isSelected)
  • смешивание создаёт путаницу между “что реально приходит” и “что нужно UI”

✅ Хорошо

// DTO (контракт API)
export interface UserDto {
id: string;
name: string;
};

// UI model (для интерфейса)
export interface UserViewModel extends UserDto {
isSelected: boolean;
};

❌ Плохо

export interface UserDto {
id: string;
name: string;
isLoading: boolean; // UI-логика в DTO
};

Структура DTO должна отражать структуру API

MUST

Файловая структура DTO должна зеркалить структуру API-документации (modules/sections/routes), чтобы типы было легко находить.

✅ Пример структуры

shared/types/dto/
users/
user.dto.ts
update-user.dto.ts
orders/
order.dto.ts
billing/
invoice.dto.ts

Правила для unknown, as и guards

MUST

  • unknown допустим только вместе с проверкой.
  • Вместо as предпочитать guards/type predicates.

✅ Хорошо (type guard)

interface UserDto { id: string; name: string };

export function isUserDto(input: unknown): input is UserDto {
if (typeof input !== 'object' || input === null) {
return false;
}

const v = input as Record<string, unknown>;

return typeof v.id === 'string' && typeof v.name === 'string';
}

❌ Плохо

const user = payload as UserDto; // каст без проверки

Процедуры контроля (Code Review)

Автор PR должен убедиться, что:

  • включён strict mode
  • нет any
  • unknown всегда сопровождается проверкой
  • as не используется для обхода компилятора (если используется, есть обоснование)
  • выбранный подход типизации соблюдается (GraphQL/OpenAPI/Schema-first/Manual)
  • generated types не коммитятся (если используется генерация)
  • DTO не смешиваются с UI типами
  • структура DTO отражает структуру API