Пошук уроків, статей та іншого контенту
Створюйте власні type guards із предикатами типів для перевірки складних структур даних.
TypeScript може звужувати типи на основі умов:
function printLength(value: string | string[]): void {
if (typeof value === "string") {
console.log(value.length);
} else {
console.log(value.length);
}
}Але стандартних перевірок недостатньо для складних структур. Наприклад, typeof value === "object" лише показує, що значення є об’єктом. TypeScript не дізнається, які саме властивості цей об’єкт має.
User-defined type guard — це функція перевірки, яка повертає предикат типу:
value is SomeTypeТакий результат повідомляє TypeScript: якщо функція повернула true, параметр value можна вважати типом SomeType.
Загальний синтаксис:
function isSomeType(value: unknown): value is SomeType {
// перевірки
}Розглянемо тип користувача та функцію перевірки:
interface User {
id: number;
name: string;
email: string;
}
function isUser(value: unknown): value is User {
if (typeof value !== "object" || value === null) {
return false;
}
const user = value as Record<string, unknown>;
return (
typeof user.id === "number" &&
typeof user.name === "string" &&
typeof user.email === "string"
);
}Тип unknown підходить для даних, отриманих із зовнішнього джерела: JSON, API, файлу або користувацького вводу. На відміну від any, він не дозволяє використовувати значення без попередньої перевірки.
Умова value is User означає:
функція повертає звичайне булеве значення;
якщо результат true, TypeScript звужує тип value до User;
відповідальність за правильність перевірки лежить на розробнику.
Тепер guard можна використовувати в умовному операторі:
function printUser(value: unknown): void {
if (isUser(value)) {
console.log(`${value.name} <${value.email}>`);
} else {
console.log("Значення не є коректним користувачем");
}
}Усередині гілки if властивості name та email доступні без додаткових перевірок.
Складні структури потрібно перевіряти на кожному необхідному рівні. Наприклад, користувач може мати адресу:
interface Address {
city: string;
country: string;
}
interface User {
id: number;
name: string;
address: Address;
}Спочатку створимо guard для адреси:
function isAddress(value: unknown): value is Address {
if (typeof value !== "object" || value === null) {
return false;
}
const address = value as Record<string, unknown>;
return (
typeof address.city === "string" &&
typeof address.country === "string"
);
}Потім використаємо його в перевірці користувача:
function isUser(value: unknown): value is User {
if (typeof value !== "object" || value === null) {
return false;
}
const user = value as Record<string, unknown>;
return (
typeof user.id === "number" &&
typeof user.name === "string" &&
isAddress(user.address)
);
}Виклик isAddress(user.address) не лише перевіряє значення під час виконання, а й допомагає TypeScript визначити, що user.address має тип Address.
Перевірка Array.isArray(value) встановлює лише факт, що значення є масивом. Тип його елементів потрібно перевірити окремо.
Можна створити універсальний guard для масивів:
function isArrayOf<T>(
value: unknown,
itemGuard: (item: unknown) => item is T
): value is T[] {
return Array.isArray(value) && value.every(itemGuard);
}Функція приймає інший guard і перевіряє всі елементи масиву через every.
Тепер можна перевірити масив користувачів:
interface User {
id: number;
name: string;
}
function isUser(value: unknown): value is User {
if (typeof value !== "object" || value === null) {
return false;
}
const user = value as Record<string, unknown>;
return (
typeof user.id === "number" &&
typeof user.name === "string"
);
}
function isArrayOf<T>(
value: unknown,
itemGuard: (item: unknown) => item is T
): value is T[] {
return Array.isArray(value) && value.every(itemGuard);
}
function printUserNames(value: unknown): void {
if (!isArrayOf(value, isUser)) {
console.log("Очікувався масив користувачів");
return;
}
for (const user of value) {
console.log(user.name);
}
}Після перевірки value має тип User[], тому кожен елемент циклу має тип User.
Нижче наведено приклад, який можна скомпілювати TypeScript-компілятором із суворими перевірками:
interface Address {
city: string;
country: string;
}
interface User {
id: number;
name: string;
address: Address;
}
function isAddress(value: unknown): value is Address {
if (typeof value !== "object" || value === null) {
return false;
}
const address = value as Record<string, unknown>;
return (
typeof address.city === "string" &&
typeof address.country === "string"
);
}
function isUser(value: unknown): value is User {
if (typeof value !== "object" || value === null) {
return false;
}
const user = value as Record<string, unknown>;
return (
typeof user.id === "number" &&
typeof user.name === "string" &&
isAddress(user.address)
);
}
function isArrayOf<T>(
value: unknown,
itemGuard: (item: unknown) => item is T
): value is T[] {
return Array.isArray(value) && value.every(itemGuard);
}
const data: unknown = [
{
id: 1,
name: "Олена",
address: {
city: "Львів",
country: "Україна",
},
},
{
id: 2,
name: "Андрій",
address: {
city: "Київ",
country: "Україна",
},
},
];
if (isArrayOf(data, isUser)) {
for (const user of data) {
console.log(`${user.name}: ${user.address.city}`);
}
} else {
console.log("Дані мають неправильну структуру");
}У цьому прикладі:
data спочатку має безпечний тип unknown.
isArrayOf перевіряє, що data є масивом.
isUser перевіряє кожен елемент.
isAddress перевіряє вкладену адресу.
Після успішної перевірки data має тип User[].
Type guard також зручно використовувати для об’єднань типів.
interface SuccessResponse {
status: "success";
data: string[];
}
interface ErrorResponse {
status: "error";
message: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;Можна перевірити конкретний варіант відповіді:
function isSuccessResponse(
value: unknown
): value is SuccessResponse {
if (typeof value !== "object" || value === null) {
return false;
}
const response = value as Record<string, unknown>;
return (
response.status === "success" &&
Array.isArray(response.data) &&
response.data.every((item) => typeof item === "string")
);
}
function handleResponse(value: unknown): void {
if (isSuccessResponse(value)) {
for (const item of value.data) {
console.log(item);
}
} else {
console.log("Помилка або некоректна відповідь");
}
}Порівняння response.status === "success" перевіряє дискримінант об’єднання, а додаткові умови перевіряють самі дані.
Type guard можна зберігати у змінній або передавати як аргумент:
type Guard<T> = (value: unknown) => value is T;
function filterValid<T>(
values: unknown[],
guard: Guard<T>
): T[] {
return values.filter(guard);
}Приклад використання:
const values: unknown[] = [
"TypeScript",
42,
"JavaScript",
null,
];
const strings = filterValid(values, (value): value is string => {
return typeof value === "string";
});
console.log(strings);
// ["TypeScript", "JavaScript"]Явний запис value is string у функції-колбеку потрібен, щоб filter повернув string[], а не просто unknown[].
Звичайна функція, яка повертає boolean, не обов’язково звужує тип:
function isString(value: unknown): boolean {
return typeof value === "string";
}
function printValue(value: unknown): void {
if (isString(value)) {
// value усе ще має тип unknown
// console.log(value.toUpperCase());
}
}Щоб TypeScript використав результат для звуження типу, потрібно вказати предикат:
function isString(value: unknown): value is string {
return typeof value === "string";
}
function printValue(value: unknown): void {
if (isString(value)) {
console.log(value.toUpperCase());
}
}Різниця полягає не в поведінці функції під час виконання, а в її типізованому контракті.
TypeScript не перевіряє, чи справді реалізація guard відповідає предикату.
function isNumber(value: unknown): value is number {
return true;
}Така функція компілюється, але є некоректною: вона повідомляє TypeScript, що будь-яке значення є числом.
const value: unknown = "text";
if (isNumber(value)) {
// TypeScript вважає value числом,
// хоча під час виконання це рядок
console.log(value.toFixed(2));
}Отже, type guard може створити розбіжність між статичною типізацією та реальною поведінкою програми. Перевірки повинні точно відповідати типу, який вказано після is.
typeof value === "object" без перевірки nullУ JavaScript typeof null дорівнює "object":
function isObject(value: unknown): value is object {
return typeof value === "object";
}Така перевірка пропускає null. Для об’єктних структур використовуйте обидві умови:
typeof value === "object" && value !== nullПеревірка через оператор in показує, що властивість існує, але не перевіряє її тип:
function hasName(value: unknown): boolean {
return (
typeof value === "object" &&
value !== null &&
"name" in value
);
}Значення name може бути числом, null або іншим типом. Для надійного guard потрібно перевірити і саме значення властивості:
function isNamedObject(
value: unknown
): value is { name: string } {
if (typeof value !== "object" || value === null) {
return false;
}
const object = value as Record<string, unknown>;
return typeof object.name === "string";
}any замість unknownany вимикає більшість перевірок TypeScript і дозволяє випадково звертатися до неіснуючих властивостей. Для неперевірених даних краще використовувати unknown, а потім звужувати його за допомогою guard.
Якщо guard перевіряє тільки одну властивість, він не повинен заявляти, що значення має повний складний тип:
interface Product {
id: number;
title: string;
price: number;
}
// Недостатня перевірка для повного типу Product
function isProduct(value: unknown): value is Product {
return typeof value === "object" && value !== null;
}Guard має перевірити всі властивості, від яких залежить подальший код.
User-defined type guard — це функція з результатом у формі value is Type.
Такий предикат звужує тип значення після успішної перевірки.
Для зовнішніх даних використовуйте unknown, а не any.
Складні об’єкти перевіряйте на кожному вкладеному рівні.
Для масивів перевіряйте і сам масив, і кожен його елемент.
TypeScript довіряє реалізації guard, тому перевірки повинні бути повними та коректними.
Звичайного результату boolean недостатньо для автоматичного звуження типу.