TypeScript в реальных проектах: Как избегать any и повышать надежность кода

#TypeScript #React #Frontend #BestPractices #CodeQuality #JavaScript

Устали от скрытых багов и any? Разбираем продвинутые техники TypeScript для создания неубиваемых React-приложений: strict-режим, generics, type guards, утилитарные типы и защита от "мусорных данных". Кейсы из e-commerce и FinTech.

TypeScript в реальных проектах

Почему any — это технический долг

.ts
// До: Хрупкий код с any
const processData = (data: any) => {
  return data.map(item => item.name.toUpperCase()); // Crash при неверной структуре
};

// После: Предсказуемое поведение
type DataItem = { id: string; name: string };
const processData = (data: DataItem[]) => {
  return data.map(item => item.name.toUpperCase()); // Гарантия структуры
};
  • Последствия any: Скрытые runtime-ошибки, снижение скорости разработки, сложный рефакторинг

5 Стратегий для полного отказа от any

1. Активируйте strict режим — ваш фундамент

tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true
  }
}
  • Эффект: Автоматическая проверка 20+ сценариев, ошибки на этапе компиляции

2. Используйте Generics для переиспользуемых компонентов

Проблема: Типизация компонента таблицы для разных данных

.tsx
// Универсальный компонент:
type TableColumn<T> = {
  key: keyof T;
  title: string;
  render?: (item: T) => React.ReactNode;
};

function DataTable<T>({ data, columns }: { 
  data: T[]; 
  columns: TableColumn<T>[] 
}) {
  return (
    <table>
      {data.map((item) => (
        <tr>
          {columns.map(col => (
            <td>{col.render ? col.render(item) : item[col.key]}</td>
          ))}
        </tr>
      ))}
    </table>
  );
}

// Использование с конкретным типом:
type Product = { id: string; name: string; price: number };
const products: Product[] = [...];

<DataTable 
  data={products}
  columns={[
    { key: "name", title: "Название" },
    { 
      key: "price", 
      title: "Цена", 
      render: (item) => `$${item.price.toFixed(2)}` 
    }
  ]}
/>

3. Type Guards для работы с внешними данными

Антипаттерн: Слепая вера в бекенд

.ts
// Опасный подход:
fetch('/api/user').then(res => res.json() as Promise<User>);

// Решение: Валидация + Type Guards
type Validated<T> = T & { __validated: true };

function isUser(data: unknown): data is User {
  return (
    typeof data === "object" &&
    data !== null &&
    "id" in data &&
    typeof data.id === "string" &&
    "email" in data &&
    typeof data.email === "string" &&
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)
  );
}

const validateResponse = async (response: unknown): Promise<Validated<User>> => {
  if (isUser(response)) {
    return { ...response, __validated: true };
  }
  throw new Error("Invalid user data");
};

4. Утилитарные типы для сложных сценариев

Кейс: Типизация формы с динамическими полями

.ts
// Упрощаем сложные типы:
type FormField<T> = {
  value: T;
  isValid: boolean;
  error?: string;
};

type UserForm = {
  name: FormField<string>;
  email: FormField<string>;
  preferences: {
    newsletter: FormField<boolean>;
    theme: FormField<'light' | 'dark'>;
  };
};

// Глубокое обновление:
type DeepPartial<T> = T extends object 
  ? { [K in keyof T]?: DeepPartial<T[K]> } 
  : T;

const updateForm = (form: UserForm, update: DeepPartial<UserForm>) => {
  // Безопасное обновление вложенных полей
};

5. Template Literal Types для динамических строк

Практическое применение: Типизация API роутов

.ts
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

type ApiRoute<M extends HttpMethod, P extends string> = 
  `${M} /api/${P}`;

// Автодополнение и проверка:
const callApi = <M extends HttpMethod, P extends string>(
  route: ApiRoute<M, P>
) => { /* реализация */ };

// Правильно:
callApi('GET /api/users'); 

// Ошибка типизации:
callApi('PATCH /api/posts'); // TS: "PATCH" не входит в HttpMethod

Антипаттерны: что делать, если "очень хочется any"

.ts
// ❌ Плохо
const parseJSON = (json: string): any => JSON.parse(json);

// ✅ Решение 1: Generic с ограничением
const parseJSON = <T>(json: string): T => JSON.parse(json) as T;

// ✅ Решение 2: Безопасное приведение
const parseJSON = (json: string): unknown => JSON.parse(json);

function isUserArray(data: unknown): data is User[] {
  return Array.isArray(data) && data.every(isUser);
}

Результаты внедрения strict TypeScript

МетрикаДо TSПосле TSЭффект
Runtime-ошибки24/мес2/мес-91%
Время на ревью кода8 ч5 ч-37%
Уверенность при рефакторингеНизкаяВысокая+💪

Итог

TypeScript — это не про аннотации типов, а про проектирование надежных систем. Отказ от any требует дисциплины, но окупается:

  • 🛡️ Защита от ошибок на этапе компиляции
  • 📚 Самодокументирующийся код
  • 🚀 Ускорение разработки за счет автодополнения
  • 🔄 Безопасный рефакторинг даже в крупных кодовых базах

Следующие шаги:

  • Включите strict: true в tsconfig.json
  • Введите ESLint-правило @typescript-eslint/no-explicit-any
  • Начните с малого: типизируйте новые модули
  • Используйте утилиты типа Partial, Pick, Omit