Коли відбувається звуження типів?
Ключова ідея
Звуження типів (type narrowing) відбувається, коли TypeScript отримує достатньо інформації, щоб виключити деякі варіанти з об'єднаного або невизначеного типу.
Тобто:
- У нас є змінна з "широким" типом (
string | number | null | undefined | ...), - Відбувається якась логічна умова або перевірка,
- TypeScript робить висновок: "Раз виконано цю умову, значить залишилися лише такі варіанти типу".
Коли саме відбувається narrowing
TypeScript автоматично звужує тип за таких подій:
| Категорія | Механізм | Приклад |
|---|---|---|
| 1. Перевірки через оператори | typeof, instanceof, in | if (typeof x === "string") |
| 2. Перевірки рівності / нерівності | ===, !==, ==, != | if (x === null) |
| 3. Перевірки truthy / falsy | if (x), !x, x && y, `x | |
| 4. Користувацькі type guards | value is Type | if (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
function example(value: string | number | boolean) {
if (typeof value === "string") {
// value: string
} else if (typeof value === "number") {
// value: number
} else {
// value: boolean
}
}if (x instanceof Date) {
// x: Date
}if ("length" in obj) {
// obj: { length: number } або подібне
}2. Перевірки рівності
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 перевірки
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
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. Дискриміновані об'єднання
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. Управління потоком
function example(x: string | null | undefined) {
if (x == null) return; // виключає null і undefined
// Тут x: string
console.log(x.toUpperCase());
}TypeScript бачить, що після return "гілка" з null/undefined відсікається,
і далі залишаються лише інші варіанти.
7. Логічні вирази
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
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 - більше немає варіантів.
Коли звуження не відбувається
- Якщо тип занадто широкий (
any) - TS не звужує:
let value: any;
if (typeof value === "string") {
// value: any - TS нічого не знає
}- Якщо перевірка не дає однозначної інформації:
let v: string | number;
if (v) {
// все ще string | number
}- Якщо TS не може відстежити контроль потоку (наприклад, при мутації змінної).
У який момент компіляції це відбувається
TypeScript виконує control flow analysis (CFA) - аналіз шляху виконання програми, крок за кроком:
- Сканує кожну інструкцію (
if,return,throw,try,switch); - Будує "граф станів типів" (type flow graph);
- У кожній гілці зберігає "поточний тип змінної";
- Після виходу з блока - об'єднує можливі типи назад (union merge).
Приклад: як думає TypeScript
let x: string | number | null;
if (x === null) {
// x: null
} else if (typeof x === "string") {
// x: string
} else {
// x: number
}Внутрішньо TS робить приблизно так:
Вихідний тип: 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), вбудований у компілятор |
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.