Skip to main content

Що означає "control flow based narrowing"?

Що таке Control Flow Based Narrowing

Control flow-based narrowing - це механізм TypeScript, який аналізує потік виконання програми (control flow) і автоматично звужує тип змінних залежно від логічних умов, перевірок і гілок коду.

Простіше кажучи:

TypeScript стежить за тим, які перевірки ти робиш, і "розуміє", які типи залишаються можливими в кожній точці програми.


Приклад 1. Базовий приклад - if

javascript
function printId(id: string | number) { if (typeof id === "string") { // Тут TS бачить: id - точно string console.log(id.toUpperCase()); } else { // А тут: id - точно number console.log(id.toFixed(2)); } }

TypeScript аналізує контроль потоку (if/else), і звузив тип id у кожній гілці.


"Control flow" - це шлях виконання програми

TypeScript усередині себе будує граф потоку керування (control flow graph, CFG):

  • кожна гілка if, else, return, throw, switch, try/catch - це окрема "гілка" потоку;
  • у кожній гілці змінні можуть мати різні типи;
  • при поверненні в загальний блок TS "об'єднує" типи назад.

Візуально:

javascript
let x: string | number; if (typeof x === "string") { // гілка 1 -> x: string } else { // гілка 2 -> x: number } // після if -> x: string | number (об'єднано)

Приклад 2. Перевірки null і undefined

javascript
function greet(name?: string | null) { if (!name) return; // відсікає null і undefined // Тут name: string console.log("Hello, " + name.toUpperCase()); }

TypeScript відстежує, що після if (!name) return можливі типи null і undefined виключені з об'єднання.


Приклад 3. Звуження через return і throw

TypeScript розуміє, що якщо виконання переривається, то гілка, що залишилася, має інший тип.

javascript
function ensureString(value: string | undefined): string { if (value === undefined) { throw new Error("Expected string"); } // Тут value: string (undefined виключено) return value.toUpperCase(); }

TS знає: "якщо throw не виконався, значить value точно не undefined".


Приклад 4. Звуження через in, instanceof, typeof

javascript
type Cat = { meow: () => void }; type Dog = { bark: () => void }; function speak(pet: Cat | Dog) { if ("bark" in pet) { // pet: Dog pet.bark(); } else { // pet: Cat pet.meow(); } }

TypeScript використовує перевірку властивості як логічний доказ, щоб виключити невідповідний тип.


Приклад 5. Комбінований потік звужень

javascript
function example(value: string | number | null) { if (value == null) { // value: null return; } if (typeof value === "string") { // value: string console.log(value.toUpperCase()); } else { // value: number console.log(value.toFixed(2)); } }

TS крок за кроком аналізує контроль потоку:

javascript
вихідний тип: string | number | null -> після if (value == null): null виключено -> після if (typeof value === "string"): number виключено

Приклад 6. Звуження при присвоєннях

javascript
let x: string | number = 42; if (typeof x === "number") { x = x + 1; // x: number } else { x = x.toUpperCase(); // x: string }

TypeScript навіть відстежує, як змінюються типи після присвоєння, це теж частина control flow analysis.


Приклад 7. Звуження всередині тернарних операторів

javascript
function format(value: string | number) { return typeof value === "string" ? value.toUpperCase() : value.toFixed(2); }

TS звужує типи по кожній гілці тернарного виразу.


Приклад 8. У зв'язці з never

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

javascript
type Shape = | { kind: "circle"; r: number } | { kind: "square"; s: number }; function area(shape: Shape) { switch (shape.kind) { case "circle": return Math.PI * shape.r ** 2; case "square": return shape.s ** 2; default: const _exhaustive: never = shape; // TS перевіряє, що сюди не потрапимо return _exhaustive; } }

Це приклад дискримінованого звуження, що ґрунтується на аналізі потоку (switch/case).


Як TypeScript робить це "під капотом"

  1. Будує граф потоку керування (Control Flow Graph):
  • кожна точка розгалуження (if, return, throw, catch) створює вузол.
  1. Присвоює кожному вузлу можливі типи.
  2. Стежить за винятками і поверненнями, де гілка завершується.
  3. Об'єднує типи назад, коли потоки сходяться.

Це й називається control flow analysis, аналіз типів у контексті виконання програми.


Приклад 9. Навіть складні вирази TS розуміє

javascript
function test(a?: number, b?: number) { if (a && b) { // a: number, b: number } else if (a) { // a: number, b: undefined } else { // a: undefined, b: number | undefined } }

TS обчислює типи в кожній гілці залежно від того, які перевірки спрацювали.


Приклад 10. Звуження після перевірки try/catch

javascript
function safeParse(input: string): object | null { try { return JSON.parse(input); } catch { return null; } }

TS знає:

  • усередині try результат object,
  • усередині catch - null. Це теж контроль потоку.

Чому це важливо

Без "control flow based narrowing" TypeScript був би просто "типовим валідатором", що не розуміє логіку програми.

А з ним:

  • компілятор аналізує реальні розгалуження і перевірки;
  • типи автоматично звужуються там, де це логічно виправдано;
  • IDE показує контекстно-залежні підказки;
  • ти можеш не боятися null, undefined, union і unknown - TS захищає від помилок на рівні потоку виконання.

Підсумок

ТермінЗначення
Control flowПотік виконання програми: if, else, return, throw, switch, try
NarrowingЗвуження множини можливих типів до конкретного
Control flow-based narrowingКоли TS автоматично звужує типи, аналізуючи логіку і розгалуження коду
Головна ідеяТип змінної змінюється залежно від того, що "доведено" в поточній гілці
РезультатМенше as, менше помилок, більше безпеки й автодоповнення

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

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

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