Понимание сужения типов в TypeScript
Вы написали type guard, но TypeScript всё равно жалуется, что свойство не существует. Или вы отфильтровали массив, но результирующий тип остаётся объединением. Эти проблемы возникают из-за разрыва между тем, как вы думаете, что работает сужение типов, и тем, как на самом деле работает анализ потока управления TypeScript.
Эта статья поможет построить чёткую ментальную модель сужения типов в TypeScript — как компилятор отслеживает типы в вашем коде и когда он теряет эту информацию.
Ключевые выводы
- Сужение типов — это анализ потока управления TypeScript, который отслеживает типы через пути выполнения на основе проверок времени выполнения, которые он может верифицировать
- Основные механизмы сужения включают
typeof,instanceof, проверки истинности, проверки равенства, операторinи дискриминируемые объединения - Пользовательские type guards с предикатами типов позволяют создавать собственное сужение, но TypeScript не проверяет логику вашего предиката
- Тип
neverобеспечивает проверку полноты охвата на этапе компиляции для выявления необработанных случаев - Сужение типов нарушается на границах callback-функций, при переназначении свойств и в сложных паттернах с псевдонимами
Как работает анализ потока управления
Сужение типов в TypeScript — это не магия. Это компилятор, следующий путям выполнения вашего кода и обновляющий информацию о типах на основе проверок времени выполнения, которые он может верифицировать.
Когда вы пишете условие, TypeScript анализирует, что должно быть истинным в каждой ветви:
function process(value: string | number) {
if (typeof value === 'string') {
// TypeScript знает: value здесь string
return value.toUpperCase()
}
// TypeScript знает: value здесь number
return value.toFixed(2)
}
Компилятор видит проверку typeof, распознаёт её как конструкцию сужения и соответствующим образом уточняет тип. Это происходит автоматически для нескольких операторов JavaScript.
Основные механизмы сужения
typeof и instanceof
Оператор typeof надёжно сужает примитивные типы. TypeScript понимает его особенности — включая то, что typeof null возвращает "object".
function handle(x: unknown) {
if (typeof x === 'string') return x.length
if (typeof x === 'number') return x.toFixed(2)
}
Для экземпляров классов instanceof обеспечивает аналогичное сужение:
if (error instanceof TypeError) {
console.log(error.message)
}
Проверки истинности и равенства
Проверки истинности отсекают null и undefined:
function greet(name: string | null) {
if (name) {
return `Hello, ${name}` // name имеет тип string
}
}
Проверки равенства сужают до литеральных типов:
type Status = 'pending' | 'complete' | 'failed'
function handle(status: Status) {
if (status === 'complete') {
// status имеет тип 'complete'
}
}
Оператор in и дискриминируемые объединения
Оператор in проверяет наличие свойства, обеспечивая паттерны утиной типизации:
if ('radius' in shape) {
return Math.PI * shape.radius ** 2
}
Дискриминируемые объединения комбинируют это с литеральными типами для мощных паттернов сужения:
type Result<T> =
| { success: true; data: T }
| { success: false; error: string }
function handle<T>(result: Result<T>) {
if (result.success) {
return result.data // TypeScript знает, что data существует
}
return result.error // TypeScript знает, что error существует
}
Discover how at OpenReplay.com.
Пользовательские type guards
Когда встроенных проверок недостаточно, предикаты типов позволяют определять собственные guards:
function isString(value: unknown): value is string {
return typeof value === 'string'
}
Одна оговорка: TypeScript не проверяет логику вашего предиката. Вы утверждаете взаимосвязь — если ваша проверка неверна, типы будут неверными.
Современный TypeScript (5.5+) улучшился в этом плане. Array.filter теперь может автоматически выводить предикаты типов для простых callback-функций, устраняя распространённую проблему:
const mixed: (string | number)[] = ['a', 1, 'b', 2]
const strings = mixed.filter(x => typeof x === 'string')
// TypeScript во многих случаях выводит string[]
Проверка полноты охвата с помощью never
Тип never обеспечивает проверку полноты охвата на этапе компиляции:
type Circle = { kind: 'circle'; radius: number }
type Square = { kind: 'square'; side: number }
type Triangle = { kind: 'triangle'; base: number; height: number }
type Shape = Circle | Square | Triangle
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2
case 'square': return shape.side ** 2
case 'triangle': return 0.5 * shape.base * shape.height
default:
const _exhaustive: never = shape
return _exhaustive
}
}
Если вы добавите новый вариант фигуры, TypeScript выдаст ошибку при присваивании never — заставляя вас обработать его.
Что не является сужением типов
Две конструкции, часто путаемые с сужением, заслуживают пояснения:
Утверждения типов (as) полностью обходят систему типов. Они не сужают — они переопределяют.
Оператор satisfies проверяет, что выражение соответствует типу, не изменяя его. Полезен для выявления ошибок, но не является механизмом сужения.
Когда сужение типов нарушается
Анализ потока управления TypeScript имеет ограничения. Сужение не сохраняется через:
- Границы callback-функций (хотя замыкания с переменными
constтеперь сохраняют сужение в TypeScript 5.4+) - Переназначения свойств между проверками
- Сложные паттерны с псевдонимами
Когда сужение неожиданно не работает, проверьте, может ли компилятор действительно отследить поток управления от вашей проверки до использования.
Заключение
Думайте о сужении типов как о том, что TypeScript наблюдает за вашими проверками времени выполнения и соответствующим образом обновляет свои знания. Компилятор консервативен — он сужает типы только тогда, когда может доказать, что уточнение обоснованно.
Пишите проверки, которые TypeScript может отследить. Предпочитайте дискриминируемые объединения утверждениям типов. Используйте проверку полноты охвата, чтобы выявлять пропущенные случаи на этапе компиляции, а не во время выполнения.
Цель не в том, чтобы бороться с системой типов, а в том, чтобы структурировать код так, чтобы сужение работало естественно.
Часто задаваемые вопросы
Анализ потока управления TypeScript не сохраняет сужение через границы callback-функций, потому что callback может выполниться позже, когда тип переменной мог измениться. Чтобы обойти это, присвойте суженное значение переменной const перед callback-функцией или используйте type guard внутри самой callback-функции.
Type guard — это проверка времени выполнения, которую TypeScript распознаёт и использует для безопасного сужения типов. Утверждение типа с помощью 'as' говорит TypeScript рассматривать значение как конкретный тип без какой-либо проверки во время выполнения. Type guards безопаснее, потому что включают реальные проверки, в то время как утверждения могут скрывать ошибки, если ваше предположение неверно.
Используйте дискриминируемые объединения, когда у вас есть связанные типы, которые имеют общее литеральное свойство, такое как 'kind' или 'type'. Они обеспечивают автоматическое сужение в операторах switch и позволяют проверять полноту охвата. Type guards лучше подходят для валидации внешних данных или когда вы не можете изменить типы, с которыми работаете.
Обычно это означает, что TypeScript потерял след вашего сужения. Распространённые причины включают проверку свойства, а затем доступ к нему через другую ссылку, переназначение переменной между проверкой и использованием, или проверку в другой области видимости. Переместите проверку типа ближе к месту использования значения или сохраните суженное значение в новой переменной const.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.