← Курс/Частые ошибки TypeScript и как их решать#199 из 257+20 XP

Частые ошибки TypeScript и как их решать

1. Object is possibly null/undefined (TS2531)

// Ошибка:
const element = document.getElementById('app')
element.innerHTML = 'Hello'  // Object is possibly null

// Решения:
// 1. Optional chaining:
element?.innerHTML  // безопасно — ничего не делает если null

// 2. Non-null assertion (если уверены):
element!.innerHTML  // говорим TS "доверяй мне"

// 3. Type guard (лучшее решение):
if (element !== null) {
  element.innerHTML = 'Hello'  // TypeScript знает что не null
}

// 4. Nullish coalescing:
const content = element ?? document.createElement('div')

2. Property does not exist on type (TS2339)

interface User { name: string; email: string }
const user: User = { name: 'Алексей', email: 'a@b.com' }

user.age  // Property 'age' does not exist on type 'User'

// Решения:
// 1. Добавить свойство в интерфейс (правильно)
interface User { name: string; email: string; age?: number }

// 2. Использовать type assertion (когда уверены):
(user as any).age  // работает, но теряем типобезопасность

// 3. Расширить тип:
type UserWithAge = User & { age: number }

3. Type X is not assignable to type Y (TS2322)

// Ошибка:
const status: 'active' | 'inactive' = 'pending'  // не входит в union

// Решение — проверить доступные значения
type Status = 'active' | 'inactive' | 'pending'
const status: Status = 'pending'  // OK

4. Argument of type X is not assignable (TS2345)

function greet(name: string) { console.log(name) }

greet(42)  // Argument of type 'number' is not assignable to parameter of type 'string'

// Решение:
greet(String(42))  // явное преобразование
greet(42 + '')     // или через конкатенацию

// Общий паттерн — перегрузки:
function greet(name: string | number) {
  console.log(String(name))
}

5. No overload matches this call (TS2769)

// Ошибка при несовпадении аргументов перегрузки
element.addEventListener('customEvent', handler)
// No overload matches this call

// Решение:
element.addEventListener('customEvent', handler as EventListener)
// Или использовать нужную перегрузку

6. Circular reference (TS2395 / TS2456)

// Проблема: тип ссылается сам на себя бесконечно
type Tree = { value: number; children: Tree[] }  // OK — массив прерывает рекурсию

// Проблемная рекурсия:
type Infinite = { next: Infinite & { extra: string } }  // TS2456

7. Implicit any (TS7006)

// Ошибка с noImplicitAny:
function process(items) { }  // Parameter 'items' implicitly has an 'any' type

// Решения:
function process(items: unknown[]) { }    // unknown безопаснее any
function process(items: string[]) { }    // конкретный тип
function process<T>(items: T[]): T[] { } // дженерик

8. Cannot find module (TS2307)

import styles from './app.module.css'  // Cannot find module or type declarations

// Решения:
// 1. Создать declaration file:
// app.module.css.d.ts:
declare module '*.css' {
  const styles: Record<string, string>
  export default styles
}

// 2. Или добавить в tsconfig:
// { "moduleResolution": "bundler" }

9. Conversion of type X to Y may be a mistake (TS2352)

const value = 'hello' as number  // Ошибка

// Решение через double assertion (с осторожностью!):
const value = 'hello' as unknown as number

// Лучше — явное преобразование:
const value = Number('hello')  // NaN

10. This context (TS2683)

class Timer {
  count = 0
  start() {
    setInterval(function() {
      this.count++  // 'this' implicitly has type 'any'
    }, 1000)
  }
}

// Решения:
// 1. Стрелочная функция (рекомендуется):
setInterval(() => { this.count++ }, 1000)

// 2. Bind:
setInterval(function(this: Timer) { this.count++ }.bind(this), 1000)

Примеры

Демонстрация 10 типичных ошибок TypeScript с правильными решениями — паттерны защитного программирования

// Демонстрируем решения для 10 типичных ошибок TypeScript.
// В TypeScript эти паттерны предотвращают ошибки ещё на этапе компиляции.

// === 1. Null/undefined safety ===
console.log('=== 1. Null Safety ===')

function safeGetElement(id) {
  const element = { id, innerHTML: '' }  // симуляция DOM

  // TS: element?.innerHTML вместо element.innerHTML (если может быть null)
  const value = element?.innerHTML ?? 'default'
  console.log('Safe access:', value)

  // Type guard:
  function processElement(el) {
    if (el === null || el === undefined) {
      console.log('Element is null/undefined — skip')
      return
    }
    console.log('Element found:', el.id)  // TypeScript знает что не null
  }

  processElement(null)
  processElement(element)
}
safeGetElement('app')

// === 2. Безопасный доступ к свойствам ===
console.log('\n=== 2. Property Access ===')

// Паттерн "hasOwnProperty" guard
function getProperty(obj, key) {
  // TS: if (key in obj) — type narrowing
  if (typeof obj === 'object' && obj !== null && key in obj) {
    return obj[key]
  }
  return undefined
}

const user = { name: 'Алексей', email: 'alex@example.com' }
console.log('name:', getProperty(user, 'name'))   // Алексей
console.log('age:', getProperty(user, 'age'))     // undefined (нет такого поля)

// === 3. Типобезопасный union ===
console.log('\n=== 3. Union Types ===')

const VALID_STATUSES = ['active', 'inactive', 'pending']

function isValidStatus(value) {
  return VALID_STATUSES.includes(value)
}

function setStatus(status) {
  if (!isValidStatus(status)) {
    throw new Error(`Invalid status: "${status}". Expected: ${VALID_STATUSES.join(', ')}`)
  }
  return status
}

try { setStatus('unknown') } catch (e) { console.log('Error:', e.message) }
console.log('Valid:', setStatus('active'))

// === 4. Безопасное приведение типов ===
console.log('\n=== 4. Type Conversion ===')

// TS: "hello" as unknown as number — double assertion
// В JS — явное преобразование
function safeToNumber(value) {
  const num = Number(value)
  if (isNaN(num)) {
    console.warn(`Cannot convert "${value}" to number, using 0`)
    return 0
  }
  return num
}

console.log(safeToNumber('42'))    // 42
console.log(safeToNumber('abc'))   // 0 + warning
console.log(safeToNumber(true))    // 1
console.log(safeToNumber(null))    // 0

// === 5. Circular references ===
console.log('\n=== 5. Circular References ===')

// Безопасная сериализация с циклическими ссылками
function safeStringify(obj) {
  const seen = new WeakSet()
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) return '[Circular]'
      seen.add(value)
    }
    return value
  })
}

const a = { name: 'a' }
const b = { name: 'b', ref: a }
a.ref = b  // циклическая ссылка

console.log(safeStringify(a))

// === 6. Generic функции вместо any ===
console.log('\n=== 6. Generics vs any ===')

// TS: function identity<T>(x: T): T
function identity(x) { return x }

// TS: function first<T>(arr: T[]): T | undefined
function first(arr) { return arr[0] }

// TS: function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>
function pick(obj, keys) {
  return keys.reduce((result, key) => {
    if (key in obj) result[key] = obj[key]
    return result
  }, {})
}

console.log(identity(42))
console.log(first([1, 2, 3]))
console.log(pick({ name: 'Алексей', email: 'a@b.com', role: 'admin' }, ['name', 'email']))

// === 7. Context (this) ===
console.log('\n=== 7. This Context ===')

class Timer {
  constructor() { this.count = 0 }

  startBroken() {
    // TS: 'this' implicitly has type 'any' в обычных функциях
    const fn = function() {
      // this тут = undefined или globalThis, не Timer
    }
    return 'broken (this не указывает на Timer)'
  }

  startFixed() {
    // Решение 1: стрелочная функция
    const fn = () => {
      this.count++  // this = Timer (замыкание)
    }
    fn()
    return 'fixed with arrow function, count = ' + this.count
  }
}

const timer = new Timer()
console.log(timer.startFixed())

// === 8. Exhaustive checks (switch) ===
console.log('\n=== 8. Exhaustive Switch ===')

// TS: function assertNever(x: never): never
function assertNever(value, allowedValues) {
  throw new Error(`Unhandled value: "${value}". Expected one of: ${allowedValues.join(', ')}`)
}

function handleStatus(status) {
  switch (status) {
    case 'active':   return 'Активен'
    case 'inactive': return 'Неактивен'
    case 'pending':  return 'Ожидает'
    default:
      // В TS: assertNever(status) — ошибка компиляции если не все кейсы обработаны
      return assertNever(status, ['active', 'inactive', 'pending'])
  }
}

console.log(handleStatus('active'))
try { handleStatus('unknown') } catch (e) { console.log('Unhandled:', e.message) }

// === 9. Type-safe event system ===
console.log('\n=== 9. Type-safe Events ===')

function createTypedEmitter(validEvents) {
  const listeners = {}

  return {
    on(event, handler) {
      if (!validEvents.includes(event)) {
        throw new Error(`Unknown event "${event}". Valid: ${validEvents.join(', ')}`)
      }
      if (!listeners[event]) listeners[event] = []
      listeners[event].push(handler)
    },
    emit(event, data) {
      if (!validEvents.includes(event)) {
        throw new Error(`Unknown event: ${event}`)
      }
      ;(listeners[event] || []).forEach(fn => fn(data))
    }
  }
}

const emitter = createTypedEmitter(['click', 'change', 'submit'])
emitter.on('click', data => console.log('click:', data))
emitter.emit('click', { x: 10, y: 20 })
try { emitter.on('unknown', () => {}) } catch (e) { console.log('Event error:', e.message) }

// === 10. Module не найден — runtime check ===
console.log('\n=== 10. Dynamic Import Guard ===')

async function safeImport(modulePath, fallback = null) {
  try {
    // В реальности: const mod = await import(modulePath)
    // Симулируем:
    if (modulePath === 'missing-module') throw new Error(`Cannot find module '${modulePath}'`)
    return { default: 'loaded module' }
  } catch (e) {
    console.warn(`Module load failed: ${e.message}`)
    return fallback
  }
}

safeImport('missing-module').then(mod => {
  console.log('Missing module result:', mod)  // null
})