Skip to main content

Що таке typeguard у TypeScript

Type guard (захист типу) - це перевірка під час виконання, яка повідомляє компілятору TypeScript точний тип значення всередині конкретного блоку коду, звужуючи union-тип до точнішого підтипу.

Теорія

TL;DR

  • Уяви type guard як контрольно-пропускний пункт: після перевірки TypeScript точно знає, що всередині значення
  • Без guard TypeScript бачить string | number як обидва варіанти одночасно; з guard кожна гілка отримує один точний тип
  • Вбудовані варіанти: typeof для примітивів, instanceof для екземплярів класів, in для перевірки наявності властивостей
  • Користувацький guard: функція, що повертає param is SpecificType, де ключове слово is обов'язкове
  • Правило вибору: маєш union-тип або unknown/any? Додай guard перед зверненням до специфічних для типу властивостей

Швидкий приклад

typescript
function printLength(value: string | number) { if (typeof value === "string") { // TypeScript знає: тут value є string console.log(value.length); // працює } else { // TypeScript знає: тут value є number console.log(value.toFixed(2)); // працює } } printLength("hello"); // 5 printLength(3.14); // "3.14"

typeof value === "string" - це і є guard. TypeScript читає цю перевірку і оновлює знання про тип лише для цієї гілки. Жодного casting-у, жодних помилок.

Ключова різниця

Без guard TypeScript не дозволяє викликати .length на string | number, бо не може гарантувати тип під час виконання. Guard дає компілятору доказ: всередині цього блоку я перевірив. Звуження типу живе тільки в цій області, після виходу з гілки union-тип повертається.

Коли використовувати

  • Union-типи в параметрах - додай typeof або instanceof перед викликом методів, специфічних для типу
  • Відповіді API з типом unknown - перевір перед зверненням до будь-якої властивості на кшталт user.name
  • Поліморфні компоненти - використовуй "onClick" in props щоб відрізнити ButtonProps від LinkProps
  • Пропусти, якщо працюєш з одним конкретним типом - TypeScript вже знає
  • Краще ніж as - casting пропускає перевірку; guard доводить тип під час виконання

Як це обробляє компілятор

TypeScript виконує аналіз потоку управління (control flow analysis) під час перевірки типів. Він відстежує можливі типи по всіх гілках коду, розпізнає вбудовані guard-и за їх патерном, і перетинає вихідний union-тип з підтипом всередині гілки. Користувацькі guard-и працюють завдяки типу повернення is: TypeScript читає param is SpecificType як контракт і застосовує звуження в кожному місці виклику. Жодних додаткових витрат під час виконання - V8 просто виконує звичайний JavaScript.

Типові помилки

Помилка 1: використання in, але виклик неправильного методу

typescript
interface Cat { meow(): void; } interface Dog { bark(): void; } function pet(animal: Cat | Dog) { if ("meow" in animal) { animal.bark(); // Помилка: у Cat немає bark } }

in звужує за наявністю властивості. Він підтверджує що meow є, а не що bark є. Викликай те, що перевірив, або використовуй discriminated union з полем-літералом type.

Помилка 2: відсутність предикату типу is

Це найчастіша проблема на код-рев'ю. Функція виглядає правильно, але звуження тихо перестає працювати.

typescript
// Повертає boolean - звуження не відбувається function isString(val: any): boolean { return typeof val === "string"; } if (isString(x)) { x.length; // TypeScript все одно бачить any тут }

TypeScript ігнорує звичайні boolean для звуження типів. Виправлення - одне слово:

typescript
function isString(val: any): val is string { return typeof val === "string"; }

Помилка 3: instanceof для рядкових примітивів

typescript
if (x instanceof String) { // Перевіряє об'єкт-обгортку, не примітив x.toUpperCase(); }

Примітиви не є екземплярами нічого. Для рядків, чисел і булевих значень використовуй typeof.

Помилка 4: перезапис змінної всередині guard-блоку

typescript
let data: string | number = "test"; if (typeof data === "string") { data = 42; // перезапис скидає звуження } data.toFixed(); // Помилка компілятора - data знову string | number

Перезапис збиває відстеження потоку управління. Оголоси локальний const всередині блоку, якщо треба трансформувати значення.

Де зустрічається

  • React - Mantine UI використовує "onClick" in props щоб рендерити <button> або <a> з одного поліморфного компонента
  • Express - typeof req.body === "object" перед зверненням до властивостей body в middleware
  • Zod - schema.safeParse(data).success автоматично звужує результат до інферованого типу
  • Next.js - req.method === "POST" звужує union методів перед парсингом тіла запиту
  • Guard vs as - використовуй guard для даних з API або від користувача; as тільки для статичних форм де впевнений на 100%

Питання на співбесіді

Q: Що таке користувацький type guard?


A: Функція, що повертає param is SpecificType. TypeScript використовує предикат is для звуження типу в кожному місці виклику. Приклад: function isUser(obj: any): obj is User { return typeof obj.name === "string"; }.

Q: Яка різниця між in, typeof та instanceof?


A: typeof перевіряє примітивні типи: string, number, boolean, function. instanceof перевіряє екземпляри класів. in перевіряє наявність властивості на об'єкті - корисно для розрізнення інтерфейсів без спільного базового класу.

Q: Як TypeScript обробляє вкладені type guard-и?


A: Аналіз потоку управління відстежує типи через вкладені if-блоки, прогресивно звужуючи тип. Що глибше вкладеність, то вужчий тип у кожній гілці.

Q: Чому звуження іноді ламається всередині циклу?


A: TypeScript скидає звуження на кожній ітерації, бо змінна може бути перезаписана між ітераціями. Зафіксуй звужене значення в локальному const всередині тіла циклу.

Q: Що таке discriminated union і як він пов'язаний з type guard-ами? (рівень senior)


A: Discriminated union використовує спільне поле-літерал на кшталт type: "success" | "error" як дискримінант. switch по цьому полю автоматично звужує всю форму об'єкта. Це надійніше за перевірки властивостей і саме такий патерн використовує Redux Toolkit для action types.

Приклади

typeof: звуження union-типу примітивів

typescript
type Shape = string | number; function doubleIt(shape: Shape) { if (typeof shape === "number") { return shape * 2; // number } return shape.toUpperCase(); // string } console.log(doubleIt(5)); // 10 console.log(doubleIt("hello")); // "HELLO"

typeof - найпростіший guard. Після раннього return у гілці number TypeScript розуміє, що весь код нижче може бути тільки string.

in: поліморфний React-компонент

typescript
interface ButtonProps { label: string; onClick: () => void; } interface LinkProps { href: string; children: string; } function InteractiveElement(props: ButtonProps | LinkProps) { if ("onClick" in props) { // звужено до ButtonProps return <button onClick={props.onClick}>{props.label}</button>; } // звужено до LinkProps return <a href={props.href}>{props.children}</a>; }

Одна функція, дві форми, жодного casting-у. Цей патерн зустрічається в бібліотеках компонентів на кшталт Mantine.

Користувацький guard: валідація відповіді API

typescript
function isUser(obj: unknown): obj is { name: string; id: number } { return ( typeof obj === "object" && obj !== null && typeof (obj as any).name === "string" && typeof (obj as any).id === "number" ); } function processItem(item: unknown) { if (isUser(item)) { console.log(item.name.toUpperCase()); // звужено } else { console.log("Not a valid user"); } } processItem({ name: "Alice", id: 1 }); // "ALICE" processItem({}); // "Not a valid user"

Цей патерн використовує Zod всередині. Guard повертає false для всього що не відповідає схемі, тому помилки завжди обробляються безпечно без try/catch.

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

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

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

Дочитали статтю?