← Курс/Type Narrowing: сужение типов в деталях#154 из 257+25 XP

Type Narrowing: сужение типов в деталях

Что такое Type Narrowing

Type Narrowing — это процесс, при котором TypeScript **сужает** широкий тип до более конкретного в определённом блоке кода на основании проверок. TypeScript анализирует код и понимает, что внутри if (typeof x === 'string') переменная x гарантированно является строкой.

typeof guards

function processValue(value: string | number | boolean) {
  if (typeof value === 'string') {
    // value: string — TypeScript сузил тип
    return value.toUpperCase()
  }
  if (typeof value === 'number') {
    // value: number
    return value.toFixed(2)
  }
  // value: boolean — единственный оставшийся вариант
  return value ? 'да' : 'нет'
}

instanceof narrowing

instanceof работает для классов:

function formatError(error: Error | string): string {
  if (error instanceof Error) {
    // error: Error — TypeScript знает что это экземпляр Error
    return `Ошибка: ${error.message}`
  }
  // error: string
  return error
}

class ValidationError extends Error {
  constructor(public field: string, message: string) {
    super(message)
  }
}

function handle(err: Error) {
  if (err instanceof ValidationError) {
    // err: ValidationError — можно обращаться к err.field
    console.log(`Поле: ${err.field}, Сообщение: ${err.message}`)
  }
}

in operator narrowing

Оператор in проверяет наличие свойства в объекте:

interface Cat { meow(): void }
interface Dog { bark(): void }

function makeSound(animal: Cat | Dog) {
  if ('meow' in animal) {
    // animal: Cat
    animal.meow()
  } else {
    // animal: Dog
    animal.bark()
  }
}

Equality narrowing

Сравнение с конкретным значением:

function handleResponse(status: 'success' | 'error' | 'loading') {
  if (status === 'success') {
    // status: 'success'
    return 'Успешно!'
  }
  if (status === 'error') {
    // status: 'error'
    return 'Ошибка!'
  }
  // status: 'loading'
  return 'Загрузка...'
}

Truthiness narrowing

Проверка на falsy-значения:

function printLength(str: string | null | undefined) {
  if (str) {
    // str: string — null и undefined отфильтрованы
    // Но! '' (пустая строка) тоже отфильтруется — это может быть нежелательно
    console.log(str.length)
  }
}

// Более точная проверка:
function printLength(str: string | null | undefined) {
  if (str != null) {
    // str: string — только null и undefined отфильтрованы
    console.log(str.length)  // Работает даже для пустой строки
  }
}

Exhaustive check с never

Паттерн гарантированной обработки всех вариантов:

type Status = 'active' | 'inactive' | 'pending'

function describe(status: Status): string {
  switch (status) {
    case 'active':   return 'Активен'
    case 'inactive': return 'Неактивен'
    case 'pending':  return 'Ожидает'
    default:
      // Если добавить 'suspended' в Status — здесь будет ошибка TS:
      // Type 'suspended' is not assignable to type 'never'
      const _exhaustive: never = status
      throw new Error(`Необработанный статус: ${_exhaustive}`)
  }
}

Type predicates (type guards)

Пользовательские функции-охранники:

function isString(value: unknown): value is string {
  return typeof value === 'string'
}

function processValues(values: unknown[]) {
  values.filter(isString).forEach(str => {
    // str: string — TypeScript знает тип после filter с type predicate
    console.log(str.toUpperCase())
  })
}

Примеры

Все виды narrowing в JavaScript: typeof, instanceof, in, equality, truthiness

// В TS: narrowing — TypeScript автоматически сужает тип в ветках
// В JS: те же проверки, но без compile-time гарантий

// === typeof narrowing ===
function processValue(value) {
  if (typeof value === 'string') {
    return `Строка: "${value.toUpperCase()}"`
  }
  if (typeof value === 'number') {
    return `Число: ${value.toFixed(2)}`
  }
  if (typeof value === 'boolean') {
    return `Булево: ${value ? 'true' : 'false'}`
  }
  if (Array.isArray(value)) {
    return `Массив [${value.length}]: ${value.join(', ')}`
  }
  return `Объект: ${JSON.stringify(value)}`
}

console.log('=== typeof narrowing ===')
console.log(processValue('hello'))       // 'Строка: "HELLO"'
console.log(processValue(3.14))          // 'Число: 3.14'
console.log(processValue(true))          // 'Булево: true'
console.log(processValue([1, 2, 3]))     // 'Массив [3]: 1, 2, 3'
console.log(processValue({ x: 1 }))     // 'Объект: {"x":1}'

// === instanceof narrowing ===
console.log('\n=== instanceof narrowing ===')

class NetworkError extends Error {
  constructor(message, status) {
    super(message)
    this.name = 'NetworkError'
    this.status = status
  }
}

class ValidationError extends Error {
  constructor(message, field) {
    super(message)
    this.name = 'ValidationError'
    this.field = field
  }
}

function handleError(error) {
  if (error instanceof NetworkError) {
    return `Сеть (код ${error.status}): ${error.message}`
  }
  if (error instanceof ValidationError) {
    return `Валидация поля '${error.field}': ${error.message}`
  }
  if (error instanceof Error) {
    return `Ошибка: ${error.message}`
  }
  return `Неизвестно: ${error}`
}

console.log(handleError(new NetworkError('Timeout', 504)))
console.log(handleError(new ValidationError('Слишком короткое', 'password')))
console.log(handleError(new Error('Что-то пошло не так')))

// === in operator narrowing ===
console.log('\n=== in operator narrowing ===')

function describeAnimal(animal) {
  if ('meow' in animal) {
    animal.meow()  // В TS: animal: Cat
  } else if ('bark' in animal) {
    animal.bark()  // В TS: animal: Dog
  } else if ('chirp' in animal) {
    animal.chirp() // В TS: animal: Bird
  }
}

const cat = { meow: () => console.log('Мяу!'), name: 'Мурка' }
const dog = { bark: () => console.log('Гав!'), name: 'Шарик' }
const bird = { chirp: () => console.log('Чирик!'), name: 'Кеша' }

describeAnimal(cat)
describeAnimal(dog)
describeAnimal(bird)

// === Exhaustive check паттерн ===
console.log('\n=== Exhaustive check ===')

function getStatusColor(status) {
  switch (status) {
    case 'success': return '#22c55e'  // зелёный
    case 'error':   return '#ef4444'  // красный
    case 'warning': return '#f59e0b'  // жёлтый
    case 'info':    return '#3b82f6'  // синий
    default:
      throw new Error(`Необработанный статус: ${status}`)
  }
}

['success', 'error', 'warning', 'info'].forEach(status => {
  console.log(`${status}: ${getStatusColor(status)}`)
})