Что такое сужение типов (narrowing) и type guards в TypeScript?
Вопрос
Как работает сужение типов (type narrowing) и что такое type guards, в том числе пользовательские (value is Type)?
Короткий ответ
Narrowing — это процесс, при котором компилятор сужает широкий тип (например, union) до более узкого внутри определённой ветки кода на основе проверок вроде typeof, instanceof, in или пользовательской функции-предиката. Type guard — это любая такая проверка, а value is Type — синтаксис для написания собственной функции-предиката, влияющей на вывод типов.
Подробный ответ
TypeScript анализирует поток управления (control flow analysis) и автоматически сужает тип переменной после проверок:
typeof — для примитивов:
function format(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase() // value: string
}
return value.toFixed(2) // value: number
}
instanceof — для проверки принадлежности классу:
class ApiError extends Error { constructor(public code: number) { super() } }
function handle(err: Error | ApiError) {
if (err instanceof ApiError) {
console.log(err.code) // err: ApiError
}
}
in — для проверки наличия свойства, полезен для объединения объектных типов без общего дискриминатора:
interface Cat { meow(): void }
interface Dog { bark(): void }
function makeSound(animal: Cat | Dog) {
if ('meow' in animal) {
animal.meow() // animal: Cat
} else {
animal.bark() // animal: Dog
}
}
Discriminated union — паттерн, где у всех вариантов union есть общее литеральное поле ("дискриминатор"), позволяющее TypeScript сужать тип через switch/if по этому полю:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2 // radius доступен
case 'square': return shape.side ** 2 // side доступен
}
}
Пользовательские type guards нужны, когда встроенных проверок недостаточно — например, для сложной валидации данных из внешнего источника (API-ответ, unknown). Функция объявляет возвращаемый тип как value is Type: если функция вернула true, TypeScript сужает тип аргумента до Type во всех местах вызова:
interface User { id: number; email: string }
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'email' in value &&
typeof (value as User).id === 'number'
)
}
function processInput(data: unknown) {
if (isUser(data)) {
console.log(data.email) // data: User — сужено пользовательской проверкой
}
}
Без value is Type (просто boolean как возвращаемый тип) TypeScript не станет сужать тип аргумента после вызова функции — придётся использовать as, теряя безопасность.
Пример
type ApiResponse =
| { status: 'success'; data: string[] }
| { status: 'error'; error: { code: number; message: string } }
function isSuccess(res: ApiResponse): res is Extract<ApiResponse, { status: 'success' }> {
return res.status === 'success'
}
function handleResponse(res: ApiResponse) {
// typeof / discriminated union
if (res.status === 'success') {
console.log(res.data.join(', '))
return
}
// instanceof для доменных ошибок
const err = new Error(res.error.message)
if (err instanceof Error) {
console.error(`[${res.error.code}] ${err.message}`)
}
}
// пользовательский type guard, используемый для фильтрации массива
const responses: ApiResponse[] = []
const successOnly = responses.filter(isSuccess) // тип: { status: 'success'; data: string[] }[]
Дополнительные вопросы
- Чем
isв type guard отличается отasserts(function assert(v): asserts v is T)? - Как narrowing работает с замыканиями — почему TypeScript "забывает" сужение внутри вложенной функции?
- Можно ли использовать exhaustiveness checking через
neverдля гарантии обработки всех вариантов union вswitch?