Що таке 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 перед зверненням до специфічних для типу властивостей
Швидкий приклад
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, але виклик неправильного методу
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
Це найчастіша проблема на код-рев'ю. Функція виглядає правильно, але звуження тихо перестає працювати.
// Повертає boolean - звуження не відбувається
function isString(val: any): boolean {
return typeof val === "string";
}
if (isString(x)) {
x.length; // TypeScript все одно бачить any тут
}TypeScript ігнорує звичайні boolean для звуження типів. Виправлення - одне слово:
function isString(val: any): val is string {
return typeof val === "string";
}Помилка 3: instanceof для рядкових примітивів
if (x instanceof String) { // Перевіряє об'єкт-обгортку, не примітив
x.toUpperCase();
}Примітиви не є екземплярами нічого. Для рядків, чисел і булевих значень використовуй typeof.
Помилка 4: перезапис змінної всередині guard-блоку
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-типу примітивів
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-компонент
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
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.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.