Перейти к содержимому

Narrowing & Type Guards

Сужение типов — ежедневный инструмент. На интервью гоняют по всем разновидностям guards: typeof, instanceof, in, equality, custom x is T, asserts. Красный флаг — не знать, что typeof null === ‘object’ и что Array.isArray — встроенный type guard.

Поток сужения для союза:

  value: string | number | Date | null
│
▼
value === null ? ──── yes ──▶ null
│ no
▼
typeof value === ‘string’ ? ──▶ string  (.toUpperCase())
│ no
▼
typeof value === ‘number’ ? ──▶ number  (.toFixed())
│ no
▼
value instanceof Date ?      ──▶ Date    (.getTime())
│ no
▼
never (exhaustive)
type Animal = { kind: 'cat'; meow(): void } | { kind: 'dog'; bark(): void };
function speak(a: Animal) {
if ('meow' in a) a.meow(); // narrowing через in
else a.bark();
}
// custom type guard
function isString(x: unknown): x is string {
return typeof x === 'string';
}
// asserts: бросает или гарантирует тип после вызова
function assert(cond: unknown, msg = 'assert'): asserts cond {
if (!cond) throw new Error(msg);
}
function head<T>(xs: T[]): T {
assert(xs.length > 0, 'empty');
return xs[0]; // T, без undefined
}

Discriminated union сужается по литеральному дискриминатору — самый надёжный способ:

type Result =
| { ok: true; value: number }
| { ok: false; error: string };
function unwrap(r: Result): number {
if (r.ok) return r.value; // здесь Result сужен до { ok: true; value: number }
throw new Error(r.error);
}

Ловушки сужения: narrowing не сохраняется через async-замыкания для изменяемых переменных, instanceof не работает с интерфейсами (только с классами), а user-defined predicate x is T — TS слепо доверяет реализации.

// Ловушка: narrowing не работает через замыкание с mutable variable
let value: string | null = Math.random() > 0.5 ? "hello" : null;
if (value !== null) {
// value: string здесь ✅
setTimeout(() => {
// value может быть изменён снаружи — TS 4.4+ пытается сохранить narrowing,
// но только для const-захваченных переменных
console.log(value?.toUpperCase()); // безопаснее с ?.
}, 0);
}
// Правильный паттерн: захватить суженную копию
function safeAsync(x: string | null) {
if (x === null) return;
const safe = x; // const — narrowing сохраняется
setTimeout(() => console.log(safe.toUpperCase()), 0); // ✅
}
// instanceof не работает с интерфейсами
interface Printable { print(): void }
// if (x instanceof Printable) { } // ❌ компиляция: Printable — только тип
// User predicate — TS доверяет без проверки реализации
function isBigInt(x: unknown): x is bigint {
return typeof x === "number"; // ОШИБКА в реализации, но TS не замечает!
}

Итог: Используй пять основных guards + custom x is T и asserts. Дискриминатор — самый надёжный способ.