Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Звуження типів у TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Звуження типів (type narrowing)** у TypeScript уточнює union-тип до конкретного підтипу, аналізуючи перевірки в потоці управління. ```typescript function process(value: string | number) { if (typeof value === "string") { value.toUpperCase(); // value: string } else { value.toFixed(2); // value: number } } ``` **Ключове:** Все звуження відбувається на етапі компіляції. Перевірки виконуються як звичайний JavaScript без витрат у рантаймі.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Звуження типів (type narrowing)** - це те, як TypeScript уточнює змінну з широкого union-типу до конкретного підтипу, аналізуючи перевірки, які ти вже пишеш у коді. ## Теорія ### TL;DR - Уяви фейсконтроль: параметр `string | number` стає просто `string`, коли ти довів це через `typeof` - TypeScript відстежує потік управління через `if/else`, `switch` і ранні `return`, звужуючи типи в кожній гілці - Шість основних технік: `typeof`, `instanceof`, `in`, перевірка рівності, перевірка правдивості, предикати типу (`value is Type`) - Дискриміновані union-и (спільне літеральне поле `kind` або `status`) - найчистіший патерн для складних станів - Все звуження відбувається на етапі компіляції. Нульова вартість у рантаймі, перевірки виконуються як звичайний JavaScript ### Швидкий приклад ```typescript function logValue(input: string | number) { // input: string | number тут if (typeof input === "string") { // TypeScript звужує: input - це string input.toUpperCase(); // OK } else { // input: number input.toFixed(2); // OK } } ``` TypeScript бачить `typeof input === "string"` і перетинає union з `string` у цій гілці. Гілка `else` отримує те, що залишилось: `number`. Наведи курсор на `input` в IDE всередині кожної гілки - тип змінюється. ### Як компілятор звужує типи Компілятор TypeScript запускає аналіз потоку управління на AST. Для кожної гілки (`if`, `else`, `switch`, ранній `return`) він перераховує, яким може бути тип змінної в цьому місці. Оригінальний тип не змінюється. TypeScript просто знає про нього більше всередині поточного scope. Жодних витрат у рантаймі. Все стирається при компіляції. V8 бачить звичайні JavaScript-умови, нічого особливого. ### Шість технік звуження **`typeof`** для union-ів з примітивами: ```typescript function format(val: string | number | null) { if (val === null) return "N/A"; if (typeof val === "string") return val.trim(); return val.toLocaleString(); // val: number тут } ``` **`instanceof`** для екземплярів класів: ```typescript function handleEvent(event: MouseEvent | KeyboardEvent) { if (event instanceof MouseEvent) { console.log(event.clientX); // event: MouseEvent } else { console.log(event.key); // event: KeyboardEvent } } ``` **Оператор `in`** для форм об'єктів та інтерфейсів: ```typescript interface Circle { radius: number; } interface Square { size: number; } function getArea(shape: Circle | Square) { if ("radius" in shape) { return Math.PI * shape.radius ** 2; // shape: Circle } return shape.size ** 2; // shape: Square } ``` **Звуження через рівність** для дискримінованих union-ів: ```typescript type Result = | { status: "ok"; data: string } | { status: "err"; message: string }; function handle(result: Result) { if (result.status === "ok") { console.log(result.data); // result: { status: "ok"; data: string } } else { console.log(result.message); } } ``` **Звуження за правдивістю** видаляє `null`/`undefined`: ```typescript function greet(name: string | null) { if (name) { console.log(name.toUpperCase()); // name: string } } ``` **Предикати типу (type predicates)** для повторно використовуваних guard-ів: ```typescript function isString(val: unknown): val is string { return typeof val === "string"; } if (isString(input)) input.toUpperCase(); // input: string ``` ### Коли що використовувати - Union з примітивами (`string | number`) → `typeof` - Екземпляри класів → `instanceof` - Форми об'єктів та інтерфейсів → `"prop" in obj` - Стейт-машини, відповіді API, Redux-екшени → дискриміновані union-и з перевіркою рівності - Складна валідація, невідомі дані з API → предикати типу - Перевірки на `null`/`undefined` → краще `=== null`, ніж правдивість, для точності ### Типові помилки **Використання `== null` замість `=== null`:** ```typescript // Неправильно - TypeScript НЕ звужує тут function bad(value: string | null) { if (value == null) return; value.length; // Помилка: value все ще string | null } // Правильно function good(value: string | null) { if (value === null) return; value.length; // value: string } ``` Либке `== null` відловлює і `null`, і `undefined`, тому TypeScript не може впевнено звузити `string | null` до `string`. Потрібне строге `===`. **Звернення до пропси без `in`:** ```typescript interface A { x: number; } interface B { y: string; } // Неправильно - TypeScript помилиться: x не існує в B function wrong(p: A | B) { if (p.x) { ... } } // Правильно function correct(p: A | B) { if ("x" in p) { /* p: A */ } } ``` **Очікування, що звуження збережеться в колбеку:** ```typescript function process(value: string | null) { if (value !== null) { setTimeout(() => { // TypeScript може позначити помилку: value могло змінитись console.log(value.toUpperCase()); }, 1000); } value = null; // змінено після перевірки } ``` Збережи звужене значення в `const` перед колбеком: ```typescript const v = value; // v: string setTimeout(() => console.log(v.toUpperCase()), 1000); ``` **`instanceof` для примітивів:** ```typescript // Неправильно - примітиви не є екземплярами класів if (value instanceof String) { ... } // Правильно if (typeof value === "string") { ... } ``` **Забути перевірити опціональне поле після звуження union-у:** ```typescript type Action = { type: "LOAD"; data?: string } | { type: "UPDATE" }; function handle(action: Action) { if (action.type === "LOAD") { // data все ще string | undefined тут! // action.data.trim(); // Помилка if (action.data) { action.data.trim(); // OK: string } } } ``` TypeScript звужує union, але не звужує автоматично вкладені опціональні поля. Кожен рівень потребує окремої перевірки. ### Де зустрічається в реальному коді - React: дискриміновані пропси (`if (props.kind === "primary")` у поліморфних компонентах типу Chakra UI) - Redux Toolkit: звуження екшенів у редюсерах через `switch (action.type)` - tRPC/Express middleware: `if ("userId" in req.body)` для варіантів тіла запиту - Zod: `if (result.success) { const data = result.data; }` після `.safeParse()` - API-відповіді: `type ApiResponse<T> = { success: true; data: T } | { success: false; error: string }` ### Питання на співбесіді **Q:** Яка різниця між звуженням типів і type assertion (`as string`)? **A:** Звуження доводить тип через аналіз потоку управління. TypeScript перевіряє твою логіку і звужує лише тоді, коли може підтвердити коректність. Assertion - це обіцянка компілятору без жодного доказу. Якщо обіцянка хибна, отримаєш помилку в рантаймі. **Q:** Напиши type guard-функцію для об'єкта користувача з API. **A:** ```typescript function isUser(obj: unknown): obj is { name: string; email: string } { return ( typeof obj === "object" && obj !== null && "name" in obj && "email" in obj ); } ``` **Q:** Чому TypeScript може не звузити union об'єктів, якщо спільна пропса має тип `string` замість літерала? **A:** Якщо дискримінант - це `string`, а не `"loading" | "success" | "error"`, TypeScript не може розрізнити варіанти. Використовуй літеральні типи або `as const`, щоб отримати точне зіставлення. **Q:** Чи працює звуження через `await`? **A:** Так, якщо змінна не перезаписується між перевіркою та `await`. TypeScript відстежує її через асинхронний потік управління, доки нічого не мутує її між цими точками. **Q:** Як зробити switch-редюсер вичерпним через `never`? **A:** ```typescript default: const _check: never = action; return _check; ``` Якщо додаєш новий тип екшену і забуваєш його обробити, TypeScript видасть помилку на присвоєнні `never`. Гілка `default` стає страховочною сіткою на етапі компіляції. ## Приклади ### Базовий: Union з примітивами та ранній return ```typescript function format(val: string | number | null) { if (val === null) return "N/A"; if (typeof val === "string") { return val.trim(); // val: string } return val.toLocaleString(); // val: number } format(" hello "); // "hello" format(1234567); // "1 234 567" format(null); // "N/A" ``` Три типи, три гілки, кожна чітко звужена. Ранній return для `null` - найпоширеніший патерн у продакшен-коді. Я використовую його за замовчуванням у кожній utility-функції, що приймає опціональні значення, бо він тримає основну логіку плоскою і читабельною. ### Середній: React-компонент з дискримінованими пропсами ```typescript interface ButtonProps { kind: "button"; label: string; onClick: () => void; } interface LinkProps { kind: "link"; label: string; href: string; } type Props = ButtonProps | LinkProps; function InteractiveElement(props: Props) { if (props.kind === "button") { // props: ButtonProps - onClick існує тут return <button onClick={props.onClick}>{props.label}</button>; } // props: LinkProps - href існує тут return <a href={props.href}>{props.label}</a>; } ``` Поле `kind` - це дискримінант. TypeScript звужує до кожного конкретного інтерфейсу всередині відповідної гілки. Спроба звернутися до `props.href` у першій гілці - помилка компіляції. Цей патерн є в майже кожній бібліотеці компонентів, що підтримує поліморфний рендеринг. ### Просунутий: Redux-редюсер з вкладеним звуженням ```typescript type Action = | { type: "LOAD"; status: "loading" | "success" | "error"; data?: string } | { type: "UPDATE"; value: number }; interface AppState { content: string; status: string; count: number; } function reducer(state: AppState, action: Action): AppState { if (action.type === "LOAD") { // action: { type: "LOAD"; status: ...; data?: string } if (action.status === "success" && action.data) { // Без '&& action.data' data залишається string | undefined. // TypeScript не дасть присвоїти її без явної перевірки. return { ...state, content: action.data }; } return { ...state, status: action.status }; } // action: { type: "UPDATE"; value: number } return { ...state, count: action.value }; } ``` Найскладніша частина: `data` опціональна (`data?: string`). Навіть після звуження `action.type === "LOAD"` і `action.status === "success"` потрібна ще перевірка `&& action.data`, щоб звузити тип з `string | undefined` до `string`. TypeScript не звужує вкладені опціональні поля автоматично. Саме цей момент найчастіше ловить розробників mid-рівня на code review.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.