Skip to main content

Коли відбувається звуження типів?

Ключова ідея

Звуження типів (type narrowing) відбувається, коли TypeScript отримує достатньо інформації, щоб виключити деякі варіанти з об'єднаного або невизначеного типу.

Тобто:

  • У нас є змінна з "широким" типом (string | number | null | undefined | ...),
  • Відбувається якась логічна умова або перевірка,
  • TypeScript робить висновок: "Раз виконано цю умову, значить залишилися лише такі варіанти типу".

Коли саме відбувається narrowing

TypeScript автоматично звужує тип за таких подій:

КатегоріяМеханізмПриклад
1. Перевірки через операториtypeof, instanceof, inif (typeof x === "string")
2. Перевірки рівності / нерівності===, !==, ==, !=if (x === null)
3. Перевірки truthy / falsyif (x), !x, x && y, `x
4. Користувацькі type guardsvalue is Typeif (isString(val))
5. Перевірки дискримінатора (discriminated unions)if (obj.kind === "circle") або switch (obj.status)
6. Управління потоком (control flow)return, throw, break, continueпісля return решта типів виключається
7. Логічні вирази з типами!x, x && y, x ? a : bу кожній гілці свій звужений тип
8. Використання never після повного виключенняякщо TS "розуміє", що інших типів не залишилосьdefault: у switch після всіх випадків

Розглянемо кожну категорію на прикладах


1. typeof, instanceof, in

javascript
function example(value: string | number | boolean) { if (typeof value === "string") { // value: string } else if (typeof value === "number") { // value: number } else { // value: boolean } }
javascript
if (x instanceof Date) { // x: Date }
javascript
if ("length" in obj) { // obj: { length: number } або подібне }

2. Перевірки рівності

javascript
function greet(name: string | null) { if (name === null) { console.log("No name"); } else { // name: string console.log("Hello, " + name.toUpperCase()); } }

Після if (name === null) TS виключає null з об'єднання в гілці else.


3. Truthy / Falsy перевірки

javascript
function process(user?: { name: string } | null) { if (!user) { // user: null | undefined return; } // Тут user: { name: string } console.log(user.name); }

Будь-яка перевірка "на порожнечу" (if (x)) автоматично прибирає null і undefined.


4. Користувацькі Type Guards

javascript
function isString(value: unknown): value is string { return typeof value === "string"; } function print(value: unknown) { if (isString(value)) { // value: string console.log(value.toUpperCase()); } }

TS довіряє функції isString - якщо вона повертає true, значить всередині цієї гілки тип value точно string.


5. Дискриміновані об'єднання

javascript
type Shape = | { kind: "circle"; radius: number } | { kind: "square"; side: number }; function area(shape: Shape) { if (shape.kind === "circle") { // shape: { kind: "circle"; radius: number } return Math.PI * shape.radius ** 2; } else { // shape: { kind: "square"; side: number } return shape.side ** 2; } }

TS звужує тип за дискримінатором (kind).


6. Управління потоком

javascript
function example(x: string | null | undefined) { if (x == null) return; // виключає null і undefined // Тут x: string console.log(x.toUpperCase()); }

TypeScript бачить, що після return "гілка" з null/undefined відсікається, і далі залишаються лише інші варіанти.


7. Логічні вирази

javascript
function showName(user?: { name: string } | null) { // user?.name звужує тип всередині const name = user && user.name; // user: { name: string } | null | undefined if (name) { // name: string console.log(name.toUpperCase()); } }

8. Звуження до never

javascript
function handle(val: "a" | "b") { switch (val) { case "a": return "Got A"; case "b": return "Got B"; default: const _exhaustive: never = val; // TS гарантує, що сюди не потрапити return _exhaustive; } }

TypeScript розуміє, що після обробки всіх можливих випадків тип звузився до never - більше немає варіантів.


Коли звуження не відбувається

  1. Якщо тип занадто широкий (any) - TS не звужує:
javascript
let value: any; if (typeof value === "string") { // value: any - TS нічого не знає }
  1. Якщо перевірка не дає однозначної інформації:
javascript
let v: string | number; if (v) { // все ще string | number }
  1. Якщо TS не може відстежити контроль потоку (наприклад, при мутації змінної).

У який момент компіляції це відбувається

TypeScript виконує control flow analysis (CFA) - аналіз шляху виконання програми, крок за кроком:

  1. Сканує кожну інструкцію (if, return, throw, try, switch);
  2. Будує "граф станів типів" (type flow graph);
  3. У кожній гілці зберігає "поточний тип змінної";
  4. Після виходу з блока - об'єднує можливі типи назад (union merge).

Приклад: як думає TypeScript

javascript
let x: string | number | null; if (x === null) { // x: null } else if (typeof x === "string") { // x: string } else { // x: number }

Внутрішньо TS робить приблизно так:

javascript
Вихідний тип: string | number | null -> if (x === null): гілка 1 = null -> else: гілка 2 = string | number -> if (typeof x === "string"): гілка 2a = string -> else: гілка 2b = number

Підсумок

ПитанняВідповідь
Коли відбувається narrowing?Коли TypeScript бачить логічну перевірку, яка виключає частину можливих типів
Що саме він робить?"Відкидає" типи, які неможливі в поточній гілці
На основі чого?typeof, instanceof, in, ===, ==, if (x), return, throw, type guards, дискримінатори
Коли припиняється?Після виходу з гілки - TS знову об'єднує всі можливі типи
Ключова особливістьЦе автоматичний аналіз потоку виконання (CFA), вбудований у компілятор

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.