Часто переменная может содержать значение одного из нескольких типов: строку или число, объект или null, успех или ошибку. Сужение типов (type narrowing) — это то, как TypeScript внутри блока кода понимает, какой именно тип сейчас перед нами, и разрешает безопасно с ним работать.
Зачем сужать типы
Допустим, функция принимает значение, которое может быть строкой или числом:
function printId(id: string | number) {
// id.toUpperCase() — ошибка: у number нет toUpperCase
console.log(id);
}
TypeScript не даст вызвать id.toUpperCase(), потому что id может оказаться числом. Чтобы получить доступ к строковым методам, нужно сначала доказать компилятору, что в этой ветке id — точно строка. Это и есть сужение: проверка во время выполнения, которую TypeScript умеет читать и применять к типам.
Короткая формула: проверил значение условием — внутри ветки тип сузился.
typeof — для примитивов
Оператор typeof возвращает строку с названием типа значения. TypeScript понимает такие проверки и сужает тип в каждой ветке:
function printId(id: string | number) {
if (typeof id === "string") {
// здесь id: string — методы строки доступны
console.log(id.toUpperCase());
} else {
// здесь id: number
console.log(id.toFixed(2));
}
}
typeof различает примитивы: "string", "number", "boolean", "bigint", "symbol", "undefined", "function" и "object". Важная ловушка: typeof null === "object" — для отсева null нужна отдельная проверка (см. ниже).
instanceof — для классов
Когда значение может быть экземпляром класса, помогает instanceof. Он проверяет цепочку прототипов и тоже сужает тип:
function describe(value: Date | string) {
if (value instanceof Date) {
// здесь value: Date
return value.toISOString();
}
// здесь value: string
return value.trim();
}
instanceof работает с тем, что создаётся через new: встроенные Date, Error, Map, а также ваши собственные классы.
Оператор in — по наличию свойства
Если типы различаются набором полей, можно проверять наличие свойства оператором in:
interface Cat { meow(): void }
interface Dog { bark(): void }
function speak(animal: Cat | Dog) {
if ("meow" in animal) {
// здесь animal: Cat
animal.meow();
} else {
// здесь animal: Dog
animal.bark();
}
}
Это удобно, когда у вас интерфейсы без общего класса — instanceof к ним неприменим, а in подходит.
Проверки на null и undefined
Значения null и undefined — частый источник ошибок. TypeScript сужает тип и здесь:
function greet(name: string | null) {
if (name === null) {
return "Привет, гость";
}
// здесь name: string
return `Привет, ${name.toUpperCase()}`;
}
Простая проверка на «истинность» отсекает сразу и null, и undefined, и пустую строку:
function length(text?: string) {
if (!text) {
return 0; // text здесь undefined или пустая строка
}
return text.length; // text: string
}
Удобный приём — раннее возвращение (early return): отсеяли «пустой» случай в начале, а дальше работаем с гарантированно валидным значением.
Пользовательские type guards (is)
Иногда проверка сложнее одного оператора, и хочется вынести её в отдельную функцию. Чтобы TypeScript продолжал сужать тип после такой функции, она должна возвращать type predicate — выражение вида параметр is Тип:
interface User {
id: number;
email: string;
}
// функция-guard: возвращает not просто boolean, а "value is User"
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"email" in value
);
}
function handle(payload: unknown) {
if (isUser(payload)) {
// здесь payload: User
console.log(payload.email);
}
}
Запись value is User говорит компилятору: «если эта функция вернула true, считай аргумент типом User». Так вы переиспользуете нетривиальную проверку и сохраняете сужение.
Discriminated unions
Самый надёжный приём для объектов — размеченное объединение (discriminated union). У каждого варианта есть общее поле-метка (литеральный тип), по которому TypeScript однозначно различает варианты:
type Result =
| { status: "ok"; data: string }
| { status: "error"; message: string };
function render(result: Result) {
switch (result.status) {
case "ok":
// здесь result: { status: "ok"; data: string }
return result.data;
case "error":
// здесь result: { status: "error"; message: string }
return result.message;
}
}
Поле status здесь — дискриминант. Проверив его, TypeScript сразу знает, какие ещё поля доступны: в ветке "ok" есть data, в ветке "error" — message. Это безопаснее, чем проверять наличие полей по одному.
Бонус — проверка полноты. Если добавить в Result третий вариант и забыть обработать его, компилятор подскажет через приём с never:
function render(result: Result): string {
switch (result.status) {
case "ok":
return result.data;
case "error":
return result.message;
default:
// если все варианты разобраны, сюда попадает never
const exhaustive: never = result;
return exhaustive;
}
}
Работа с unknown
Тип unknown — это «безопасный any». Он означает «значение есть, но тип неизвестен», и TypeScript запрещает любые операции с ним, пока тип не сужен. Это и делает его правильным выбором для данных извне: ответ сети, разбор JSON, ввод пользователя.
function parse(json: string) {
const data: unknown = JSON.parse(json); // JSON.parse возвращает any, фиксируем как unknown
// data.id — ошибка: тип неизвестен
if (isUser(data)) {
console.log(data.id); // здесь data: User
}
}
В отличие от any, unknown заставляет вас сначала проверить значение — а значит, ошибка типа всплывёт при компиляции, а не во время выполнения. Короткая формула: данные извне принимай как unknown и сужай guard'ом перед использованием.
Почему сужение делает код безопасным
Каждая проверка-сужение работает на двух уровнях сразу. Во время выполнения это обычное условие. Во время компиляции TypeScript использует ту же проверку, чтобы запретить недопустимые операции в каждой ветке. Поэтому «у undefined нет такого метода» и подобные сбои ловятся ещё до запуска: компилятор просто не даст обратиться к полю, которого в этой ветке может не быть.
Коротко
- Сужение типов — это то, как TypeScript внутри ветки понимает конкретный тип значения из объединения.
typeofсужает примитивы,instanceof— экземпляры классов,in— по наличию свойства, сравнение сnull/undefined— отсекает «пустые» значения.- Помни ловушку
typeof null === "object"— дляnullнужна отдельная проверка. - Пользовательский type guard с возвратом
value is Типпереиспользует сложную проверку и сохраняет сужение. - Discriminated union с полем-дискриминантом — самый надёжный способ различать варианты объектов; приём с
neverдаёт проверку полноты. - Данные извне принимай как unknown и сужай guard'ом — компилятор заставит проверить тип до использования.
Что почитать дальше
- Основы системы типов — объединения, литеральные типы и из чего складываются типы, которые мы сужаем.
- Generics — обобщённые типы, которые часто комбинируют с type guards.
- Асинхронность и event loop — где
unknownособенно полезен при разборе ответов сети.