Що означає "control flow based narrowing"?
Що таке Control Flow Based Narrowing
Control flow-based narrowing - це механізм TypeScript, який аналізує потік виконання програми (control flow) і автоматично звужує тип змінних залежно від логічних умов, перевірок і гілок коду.
Простіше кажучи:
TypeScript стежить за тим, які перевірки ти робиш, і "розуміє", які типи залишаються можливими в кожній точці програми.
Приклад 1. Базовий приклад - if
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 "об'єднує" типи назад.
Візуально:
let x: string | number;
if (typeof x === "string") {
// гілка 1 -> x: string
} else {
// гілка 2 -> x: number
}
// після if -> x: string | number (об'єднано)Приклад 2. Перевірки null і undefined
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 розуміє, що якщо виконання переривається, то гілка, що залишилася, має інший тип.
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
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. Комбінований потік звужень
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 крок за кроком аналізує контроль потоку:
вихідний тип: string | number | null
-> після if (value == null): null виключено
-> після if (typeof value === "string"): number виключеноПриклад 6. Звуження при присвоєннях
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. Звуження всередині тернарних операторів
function format(value: string | number) {
return typeof value === "string" ? value.toUpperCase() : value.toFixed(2);
}TS звужує типи по кожній гілці тернарного виразу.
Приклад 8. У зв'язці з never
TypeScript може "зрозуміти", що всі варіанти оброблено,
і тип, що залишився, стає never.
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 робить це "під капотом"
- Будує граф потоку керування (Control Flow Graph):
- кожна точка розгалуження (
if,return,throw,catch) створює вузол.
- Присвоює кожному вузлу можливі типи.
- Стежить за винятками і поверненнями, де гілка завершується.
- Об'єднує типи назад, коли потоки сходяться.
Це й називається control flow analysis, аналіз типів у контексті виконання програми.
Приклад 9. Навіть складні вирази TS розуміє
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
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, менше помилок, більше безпеки й автодоповнення |
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.