TypeScriptMiddleExperience

Расскажите о случае, когда особенности TypeScript повлияли на архитектурное решение, производительность или сопровождение проекта.

TypeScript выявил скрытые архитектурные проблемы: непоследовательные формы API-ответов, избыточные ре-рендеры и отсутствие рантаймовой валидации, что привело к единой Zod-схеме как источнику истины.

Влияние TypeScript на архитектурные решения

Один из наиболее показательных случаев — рефакторинг слоя API-клиентов в крупном SPA при переходе с JavaScript на TypeScript. Типизация выявила скрытые архитектурные проблемы и повлияла на итоговое решение.

Контекст

Проект: React-приложение с 80+ API-эндпоинтами, каждый вызов через axios без типов. Симптомы: частые runtime ошибки вида Cannot read property 'x' of undefined, нет автодополнения в IDE.

Архитектурное решение: code generation vs Zod

Первый вариант — сгенерировать типы из OpenAPI через openapi-typescript:

npx openapi-typescript https://api.example.com/openapi.json -o src/types/api.d.ts

Это дало 500+ строк типов, но без рантаймовой валидации. Второй вариант — Zod-схемы как единственный источник истины:

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'viewer']),
  createdAt: z.string().datetime(),
});

type User = z.infer<typeof UserSchema>; // тип = схема, нет дублирования

async function fetchUser(id: string): Promise<User> {
  const data = await api.get(`/users/${id}`);
  return UserSchema.parse(data); // throws при несоответствии
}

Влияние на сопровождение

TypeScript выявил, что 12 из 80 эндпоинтов возвращали разные формы данных в зависимости от условий бэкенда. Это потребовало явного моделирования union-типов:

type ApiResponse<T> =
  | { status: 'ok'; data: T }
  | { status: 'error'; code: string; message: string };

async function safeGet<T>(schema: z.ZodType<T>, url: string): Promise<ApiResponse<T>> {
  try {
    const raw = await api.get(url);
    return { status: 'ok', data: schema.parse(raw) };
  } catch (e) {
    return { status: 'error', code: 'PARSE_ERROR', message: String(e) };
  }
}

Влияние на производительность

Переход на TypeScript обнаружил избыточные re-render из-за неправильной мемоизации: без типов props передавались как any, TypeScript показал, что некоторые объекты создавались inline при каждом рендере.

Подводные камни

  • Начинать миграцию с allowJs: true и checkJs: false удобно, но создаёт двухскоростную кодовую базу — установите дедлайн на удаление JS-файлов.
  • Zod.parse бросает исключение синхронно — оборачивайте в try/catch или используйте safeParse для предсказуемого контроля потока.
  • Кодогенерация из OpenAPI отстаёт от бэкенда при ручных изменениях схемы — автоматизируйте через pre-commit hook или CI шаг.
  • Строгая типизация иногда приводит к over-engineering: не все данные нужно парсить через Zod, внутренние вычисления можно оставить на уровне TypeScript типов.
  • Generic-тяжёлые утилиты замедляют language server — профилируйте через --extendedDiagnostics.
  • Рефакторинг типов в публичном API пакета монорепо ломает все потребители сразу — версионируйте внутренние пакеты или используйте changesets.
  • TypeScript не защищает от проблем с мутабельностью — используйте Readonly и as const там, где важна иммутабельность.
  • Миграция большой кодовой базы с JS должна идти модульно — не пытайтесь включить strict одним PR.

What hurts your answer

  • Выдумывать опыт или говорить слишком общими фразами
  • Не объяснять свою личную роль в работе с TypeScript
  • Не показывать результат, метрики или извлечённые уроки

What they're listening for

  • Может подготовить честный пример использования TypeScript
  • Показывает свою роль, решения и результат
  • Умеет рефлексировать над trade-offs и уроками

Related topics