TS Narrowing и Type Guards
Narrowing — процесс сужения широкого типа (union, unknown) до конкретного внутри ветки кода; type guard — выражение или функция, которая подтверждает компилятору принадлежность значения к определённому типу.
Зачем нужно
- TypeScript отслеживает поток выполнения и автоматически сужает типы в условных ветках — это называется Control Flow Analysis
- Без narrowing работа с
string | numberилиunknownтребовала бы небезопасных приведений черезas - Явные type guards позволяют вынести проверку в переиспользуемую функцию с гарантией компилятора
Где используется
- Обработка ответов API с типом
unknownили широкими union - Компоненты React с пропами-дискриминантами (
type: "text" | "image") - Обработчики ошибок (
catch (e)— типunknownв strict-режиме) - Парсинг/валидация внешних данных (JSON, FormData)
- Функции, принимающие несколько типов (перегрузки, полиморфизм)
Основной контент
typeof guard
function format(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase(); // здесь value: string
}
return value.toFixed(2); // здесь value: number
}
instanceof guard
class ApiError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
}
}
function handleError(err: unknown): string {
if (err instanceof ApiError) {
return `API ${err.statusCode}: ${err.message}`; // err: ApiError
}
if (err instanceof Error) {
return err.message; // err: Error
}
return "Неизвестная ошибка";
}
in operator guard
interface Cat { meow: void; }
interface Dog { bark: void; }
function speak(animal: Cat | Dog): void {
if ("meow" in animal) {
animal.meow; // animal: Cat
} else {
animal.bark; // animal: Dog
}
}
Discriminated union (тегированный union)
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // shape: { kind: "circle"; radius: number }
case "rect":
return shape.width * shape.height; // shape: { kind: "rect"; ... }
}
}
User-defined type guard (предикат)
interface User {
id: number;
name: string;
email: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
typeof (value as User).id === "number" &&
typeof (value as User).name === "string" &&
typeof (value as User).email === "string"
);
}
async function fetchUser(id: number): Promise<User> {
const data: unknown = await fetch(`/api/users/${id}`).then(r => r.json());
if (isUser(data)) {
return data; // data: User — компилятор доверяет предикату
}
throw new Error("Невалидный ответ API");
}
Assertion function
function assertIsString(val: unknown): asserts val is string {
if (typeof val !== "string") {
throw new TypeError(`Expected string, got ${typeof val}`);
}
}
let value: unknown = "hello";
assertIsString(value);
console.log(value.toUpperCase()); // value: string после assert
Exhaustive check с never
type Status = "active" | "inactive" | "banned";
function describe(status: Status): string {
switch (status) {
case "active": return "Активен";
case "inactive": return "Неактивен";
case "banned": return "Заблокирован";
default:
const _check: never = status;
throw new Error(`Необработанный статус: ${_check}`);
}
}
Частые ошибки
- Использовать
asвместо type guard —value as Userобходит проверку, ошибки уходят в рантайм - Предикат, который всегда возвращает true — компилятор доверяет предикату без проверки его корректности
- Не обрабатывать
nullприtypeof obj === "object"—typeof null === "object"в JavaScript - Discriminated union без общего тега — TypeScript не сможет сузить тип в switch
- Забыть ветку default с never — при добавлении нового варианта в union ошибка не возникает до рантайма