Пошук уроків, статей та іншого контенту
З’ясуйте, як TypeScript звужує широкий тип до конкретнішого на основі перевірок у коді.
Type narrowing — це процес, під час якого TypeScript звужує широкий тип змінної до конкретнішого типу на основі перевірок у коді.
Наприклад, змінна може мати тип string | number. Це означає, що в ній може бути рядок або число:
let value: string | number;
value = "TypeScript";
value = 42;Поки TypeScript не знає, яке саме значення містить value, він дозволяє використовувати лише операції, спільні для string і number.
let value: string | number = "TypeScript";
// Помилка: метод toUpperCase доступний лише для string
// value.toUpperCase();За допомогою перевірки можна повідомити TypeScript, який тип має значення в певній частині коду.
typeofОператор typeof дозволяє перевірити базовий тип значення:
function formatValue(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase();
}
return value.toFixed(2);
}
console.log(formatValue("typescript")); // TYPESCRIPT
console.log(formatValue(12.5)); // 12.50У цьому прикладі:
Параметр value має тип string | number.
Усередині if TypeScript знає, що value — це string.
Після if TypeScript знає, що value — це number.
Тому в кожній частині функції доступні відповідні методи.
typeoffunction describeValue(value: string | number | boolean): string {
if (typeof value === "string") {
return `Рядок: ${value}`;
}
if (typeof value === "number") {
return `Число: ${value}`;
}
return `Булеве значення: ${value}`;
}
console.log(describeValue("hello"));
console.log(describeValue(10));
console.log(describeValue(true));typeof може повертати, зокрема:
"string" для рядків;
"number" для чисел;
"boolean" для булевих значень;
"object" для об’єктів;
"function" для функцій.
nullТипи часто об’єднують із null, якщо значення може бути відсутнім:
function printName(name: string | null): void {
if (name === null) {
console.log("Ім'я не вказано");
return;
}
console.log(`Ім'я: ${name.toUpperCase()}`);
}
printName("Anna");
printName(null);Після перевірки name === null і завершення функції TypeScript розуміє, що далі name — це string.
Такий підхід називають раннім поверненням:
function getLength(value: string | null): number {
if (value === null) {
return 0;
}
return value.length;
}Без перевірки TypeScript не дозволив би звернутися до value.length, адже value може бути null.
TypeScript також може звужувати тип під час перевірки значення в умові:
function printMessage(message: string | null): void {
if (message) {
console.log(message.toUpperCase());
} else {
console.log("Повідомлення відсутнє або порожнє");
}
}
printMessage("Вітаю!");
printMessage(null);
printMessage("");У блоці if TypeScript знає, що message — непорожній рядок.
Проте перевірка на істинність має особливість: порожній рядок "" вважається хибним значенням. Тому така перевірка підходить не в усіх ситуаціях.
Якщо потрібно перевірити лише null, краще зробити це явно:
function printMessage(message: string | null): void {
if (message !== null) {
console.log(`Повідомлення: "${message}"`);
}
}=== та !==Тип можна звузити, порівнюючи значення з конкретним значенням:
function getStatusMessage(status: "success" | "error" | "loading"): string {
if (status === "success") {
return "Операцію виконано успішно";
}
if (status === "error") {
return "Сталася помилка";
}
return "Операція виконується";
}
console.log(getStatusMessage("success"));
console.log(getStatusMessage("loading"));Тут параметр status має об’єднаний тип із трьох конкретних рядків:
"success" | "error" | "loading"Після кожної перевірки TypeScript виключає вже перевірені варіанти.
Звуження особливо корисне для об’єднаних типів — типів, записаних через |.
type Result = string | number;
function processResult(result: Result): void {
if (typeof result === "string") {
console.log(`Отримано текст довжиною ${result.length}`);
} else {
console.log(`Отримано число: ${result * 2}`);
}
}
processResult("готово");
processResult(21);У цьому прикладі тип Result може бути рядком або числом. Перевірка typeof result === "string" розділяє ці два варіанти.
inОператор in перевіряє, чи є певна властивість в об’єкті. TypeScript використовує цю перевірку для звуження типу.
type EmailContact = {
email: string;
};
type PhoneContact = {
phone: string;
};
function contactUser(contact: EmailContact | PhoneContact): string {
if ("email" in contact) {
return `Надсилаємо лист на ${contact.email}`;
}
return `Телефонуємо за номером ${contact.phone}`;
}
console.log(contactUser({ email: "user@example.com" }));
console.log(contactUser({ phone: "+380501234567" }));Усередині if TypeScript розуміє, що contact має властивість email, а в іншій частині — властивість phone.
Цей спосіб зручний, коли варіанти об’єкта мають різні властивості.
Для об’єктів часто використовують спільну властивість-ідентифікатор. Її ще називають discriminant — розрізнювальною властивістю.
type LoadingState = {
status: "loading";
};
type SuccessState = {
status: "success";
data: string[];
};
type ErrorState = {
status: "error";
message: string;
};
type RequestState = LoadingState | SuccessState | ErrorState;
function renderState(state: RequestState): string {
switch (state.status) {
case "loading":
return "Завантаження...";
case "success":
return `Отримано елементів: ${state.data.length}`;
case "error":
return `Помилка: ${state.message}`;
}
}
console.log(renderState({ status: "loading" }));
console.log(renderState({ status: "success", data: ["A", "B"] }));
console.log(renderState({ status: "error", message: "Сервер недоступний" }));Після перевірки state.status TypeScript знає властивості відповідного об’єкта:
для "loading" доступна лише властивість status;
для "success" доступна властивість data;
для "error" доступна властивість message.
Це безпечніший підхід, ніж намагатися звертатися до всіх властивостей без перевірки.
instanceofinstanceof перевіряє, чи створений об’єкт за допомогою певного класу. TypeScript використовує цю перевірку для звуження типу.
class UserError {
constructor(public message: string) {}
}
class NetworkError {
constructor(public code: number) {}
}
function handleError(error: UserError | NetworkError): string {
if (error instanceof UserError) {
return `Помилка користувача: ${error.message}`;
}
return `Мережева помилка з кодом ${error.code}`;
}
console.log(handleError(new UserError("Некоректні дані")));
console.log(handleError(new NetworkError(500)));У першій частині error має тип UserError, а після перевірки — NetworkError.
Перевірки працюють і під час обробки масивів зі змішаними типами:
const values: Array<string | number> = ["TypeScript", 10, "JavaScript", 20];
for (const value of values) {
if (typeof value === "string") {
console.log(value.toUpperCase());
} else {
console.log(value.toFixed(1));
}
}На кожній ітерації value може бути рядком або числом. Перевірка визначає, які операції дозволені для поточного елемента.
TypeScript не аналізує довільний код настільки глибоко, щоб самостійно гарантувати правильний тип без перевірки.
function printLength(value: string | number): void {
// Помилка: у number немає властивості length
// console.log(value.length);
if (typeof value === "string") {
console.log(value.length);
}
}Потрібно явно створити умову, яка доводить TypeScript, що значення має потрібний тип.
Звуження діє лише в тій частині коду, де перевірка залишається справедливою:
function printValue(value: string | number): void {
if (typeof value === "string") {
console.log(value.toUpperCase());
return;
}
console.log(value.toFixed(2));
}Після return TypeScript може безпечно вважати, що в наступній частині value — це number.
function format(value: string | number): string {
// Помилка: value може бути number
// return value.toUpperCase();
if (typeof value === "string") {
return value.toUpperCase();
}
return value.toString();
}Спочатку перевірте тип, а потім використовуйте специфічні для нього методи.
function showName(name: string | null): void {
if (name) {
console.log(name);
}
}Цей код не виведе порожній рядок, хоча порожній рядок є коректним значенням типу string.
Якщо потрібно відрізнити null від рядка, використовуйте явну перевірку:
function showName(name: string | null): void {
if (name !== null) {
console.log(name);
}
}type Success = {
status: "success";
data: string;
};
type Failure = {
status: "failure";
error: string;
};
function getMessage(result: Success | Failure): string {
if (result.status === "success") {
return result.data;
}
return result.error;
}Не слід звертатися до result.data або result.error до перевірки status, адже кожна властивість існує лише в одному з варіантів.
Type narrowing звужує широкий тип до конкретнішого.
Найчастіше для звуження використовують:
typeof;
перевірки === та !==;
перевірки на null;
перевірку на істинність;
оператор in;
оператор instanceof;
спільну властивість-ідентифікатор у різних типах об’єктів.
Після успішної перевірки TypeScript дозволяє використовувати властивості й методи конкретного типу.
Звуження допомагає писати безпечний код без зайвих приведень типів.
Перед використанням специфічної властивості або методу спочатку переконайтеся, що значення справді має потрібний тип.