Какие есть частые антипаттерны в TypeScript?

MiddleTypeScript #typescript #antipatterns

Вопрос

Какие практики в TypeScript считаются антипаттернами и почему они опасны?

Короткий ответ

Самые частые антипаттерны — злоупотребление any (отключает проверку типов полностью), небезопасные приведения через as, злоупотребление non-null assertion (!), подавление ошибок через @ts-ignore вместо их исправления, и неочевидные ловушки числовых enum. Все они меняют компромисс "поймать ошибку сейчас" на "поймать ошибку в проде".

Подробный ответ

1. Злоупотребление any. any полностью отключает проверку типов для значения и всего, что из него выведено дальше — по сути, откат к обычному JavaScript, но с ложным ощущением безопасности:

function process(data: any) {
  return data.user.profile.name.toUpperCase() // ни одна ошибка не будет поймана
}

Правильная альтернатива — unknown (требует явного сужения перед использованием) или точный тип/дженерик.

2. Небезопасные as-приведения. as не проверяет совместимость типов на рантайм-уровне — это просто "заявление" компилятору поверить программисту. as от unknown/непроверенных данных может замаскировать неправильную форму объекта:

const response = await fetch('/api/user')
const user = (await response.json()) as User // TS верит, но структура не проверена
console.log(user.email.toLowerCase()) // упадёт в рантайме, если email отсутствует

Безопаснее — валидировать данные (вручную через type guard или библиотекой вроде zod) вместо приведения "вслепую".

3. Non-null assertion (!) "на всякий случай". Оператор ! говорит компилятору "здесь точно не null/undefined", отключая проверку — если разработчик ошибся, будет рантайм-ошибка Cannot read properties of undefined:

function getUser(id: number): User | undefined { /* ... */ return undefined }

const user = getUser(1)!  // "я уверен, что не undefined" — а если нет?
console.log(user.name)     // упадёт в рантайме

Лучше — реальная проверка (if (!user) return) или явная обработка отсутствующего значения.

4. @ts-ignore / @ts-expect-error вместо исправления типов. @ts-ignore подавляет ошибку компилятора на следующей строке без объяснения причины и без проверки, что ошибка вообще ещё существует — со временем код вокруг может измениться, а игнорируемая ошибка — превратиться в другую, незамеченную:

// @ts-ignore
const result = riskyFunction(wrongArgType) // почему это нормально? неизвестно

@ts-expect-error чуть безопаснее — вызовет собственную ошибку, если код внизу перестанет быть ошибочным (то есть проблема исчезла), но обе директивы стоит использовать точечно и с комментарием, а не как способ "заставить билд пройти".

5. Ловушки числовых enum. Числовой enum компилируется в двусторонний маппинг (значение → имя и имя → значение) и допускает присвоение любого числа переменной этого типа без ошибки:

enum Status { Pending, Success, Error }

let s: Status = 99 // OK для числового enum! Никакой проверки диапазона

Строковые enum безопаснее в этом плане, но чаще предпочитают union строковых литералов (type Status = 'pending' | 'success' | 'error') — они не генерируют дополнительный JS-код в рантайме и лучше сочетаются со структурной типизацией.

Пример

// Плохо: any, as и ! вместе
function renderBad(input: any) {
  const user = input as { name: string }
  return user!.name.toUpperCase()
}

// Хорошо: unknown + type guard + явная обработка отсутствия данных
interface User { name: string }

function isUser(value: unknown): value is User {
  return typeof value === 'object' && value !== null && 'name' in value
    && typeof (value as User).name === 'string'
}

function renderGood(input: unknown): string {
  if (!isUser(input)) {
    throw new Error('Invalid user payload')
  }
  return input.name.toUpperCase() // input сужен до User, без as и !
}

// Плохо: числовой enum без защиты от произвольных чисел
enum RoleBad { Admin, User, Guest }

// Хорошо: union литералов — та же выразительность, без рантайм-кода и без дыр
type Role = 'admin' | 'user' | 'guest'

function checkAccess(role: Role) {
  if (role === 'admin') return true
  return false
}

Дополнительные вопросы

  • Чем unknown принципиально отличается от any с точки зрения безопасности?
  • В каких редких случаях as действительно оправдан и не считается антипаттерном?
  • Почему строковые enum тоже не идеальны по сравнению с union строковых литералов, и когда enum всё же уместен?