Types поглиблено
TypeScript types — органайзер з відсіками де кожен тримає предмети лише одного типу
У попередньому уроці ми познайомились з базовими типами -- string, number, boolean. Але реальний код рідко буває таким простим. Змінна може містити рядок або число, значення може бути лише одним з кількох варіантів, а деякі функції ніколи не повертають результат. У цьому уроці розберемо ці випадки.
Union Types -- об'єднання типів
Union type описує значення, яке може бути одним з кількох типів:
// Змінна може бути string або number
let id: string | number;
id = "abc-123"; // OK
id = 42; // OK
// id = true; // Помилка: Type 'boolean' is not assignable
// Параметр функції
function printId(id: string | number): void {
console.log(`ID: ${id}`);
}
printId("abc"); // OK
printId(123); // OK
Робота з union types
Коли змінна має union type, TypeScript дозволяє використовувати тільки спільні методи обох типів:
function printId(id: string | number): void {
// console.log(id.toUpperCase()); // Помилка! toUpperCase є тільки у string
console.log(id.toString()); // OK -- toString є і у string, і у number
}
Щоб використати специфічні методи, потрібне звуження типу (type narrowing):
function printId(id: string | number): void {
if (typeof id === "string") {
// TypeScript знає, що тут id -- це string
console.log(id.toUpperCase());
} else {
// А тут -- number
console.log(id.toFixed(2));
}
}
Literal Types -- літеральні типи
Literal type -- це тип, який приймає тільки конкретне значення:
// Змінна може бути тільки "admin" або "user" або "guest"
let role: "admin" | "user" | "guest";
role = "admin"; // OK
role = "user"; // OK
// role = "superadmin"; // Помилка!
// У функції
function setTheme(theme: "light" | "dark"): void {
document.body.className = theme;
}
setTheme("light"); // OK
setTheme("dark"); // OK
// setTheme("blue"); // Помилка!
Літеральні типи працюють не тільки з рядками:
// Числові literal types
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll = 3; // OK
// roll = 7; // Помилка!
// Boolean literal
type Success = true;
const result: Success = true;
Literal types дуже корисні для обмеження допустимих значень. Замість status: string пиши status: "active" | "inactive" | "banned" -- і TypeScript не дасть помилитися.
Type Aliases -- псевдоніми типів
Type alias дає ім'я типу, щоб використовувати його повторно:
// Без alias -- повторення
function getUser(): { name: string; email: string; age: number } { /* ... */ }
function updateUser(user: { name: string; email: string; age: number }): void { /* ... */ }
// З alias -- DRY (Don't Repeat Yourself)
type User = {
name: string;
email: string;
age: number;
};
function getUser(): User { /* ... */ }
function updateUser(user: User): void { /* ... */ }
Type aliases працюють з будь-якими типами:
// Простий alias
type ID = string | number;
type Theme = "light" | "dark";
// Alias для масиву
type StringArray = string[];
type NumberList = Array<number>;
// Alias для функції
type Formatter = (value: string) => string;
// Використання
let userId: ID = "abc-123";
let currentTheme: Theme = "dark";
const toUpper: Formatter = (value) => value.toUpperCase();
Вкладені типи
type Address = {
city: string;
street: string;
zip: string;
};
type User = {
name: string;
email: string;
address: Address; // вкладений тип
};
const user: User = {
name: "Олексій",
email: "alex@test.com",
address: {
city: "Київ",
street: "Хрещатик, 1",
zip: "01001",
},
};
Tuples -- кортежі
Tuple -- це масив з фіксованою кількістю елементів та визначеним типом для кожної позиції:
// Tuple: [string, number]
let coordinates: [number, number] = [50.45, 30.52];
// Різні типи на різних позиціях
let userEntry: [string, number, boolean] = ["Олексій", 25, true];
// Доступ за індексом -- TypeScript знає тип
const name = userEntry[0]; // string
const age = userEntry[1]; // number
const active = userEntry[2]; // boolean
// Помилка -- неправильний тип на позиції
// let wrong: [string, number] = [42, "hello"]; // Помилка!
Де корисні tuples?
// Повернення кількох значень з функції (як useState в React)
function useState(initial: string): [string, (value: string) => void] {
let state = initial;
const setState = (value: string) => { state = value; };
return [state, setState];
}
const [value, setValue] = useState("Привіт");
// value: string, setValue: (value: string) => void
// Named tuple (для документації)
type UserTuple = [name: string, age: number, email: string];
Enums -- перелічення
Enum -- набір іменованих констант:
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
let move: Direction = Direction.Up;
console.log(move); // 0
// String enum (рекомендовано)
enum Status {
Active = "active",
Inactive = "inactive",
Banned = "banned",
}
let userStatus: Status = Status.Active;
console.log(userStatus); // "active"
// Використання в умовах
function getStatusLabel(status: Status): string {
switch (status) {
case Status.Active: return "Активний";
case Status.Inactive: return "Неактивний";
case Status.Banned: return "Заблокований";
}
}
Багато розробників віддають перевагу union literal types замість enum: type Status = "active" | "inactive" | "banned". Це простіше і не генерує додатковий JavaScript-код. Enum корисний, коли потрібна зворотна відповідність (значення -> ім'я).
any, unknown, never
Три спеціальних типи, які потрібно розуміти.
any -- будь-який тип (вимикає перевірку)
let value: any = "hello";
value = 42; // OK
value = true; // OK
value.foo.bar; // OK -- TypeScript не перевіряє
value(); // OK -- навіть це
any повністю вимикає перевірку типів. Це як повернутися до JavaScript.
Уникай any як чуми! Кожен any у коді -- це потенційний баг, який TypeScript не зможе знайти. Якщо не знаєш тип -- використовуй unknown.
unknown -- безпечний "будь-який тип"
let value: unknown = "hello";
value = 42; // OK -- присвоювати можна будь-що
value = true; // OK
// Але використовувати НЕ можна без перевірки!
// console.log(value.toUpperCase()); // Помилка!
// value(); // Помилка!
// Потрібно спершу перевірити тип
if (typeof value === "string") {
console.log(value.toUpperCase()); // OK -- TypeScript знає, що це string
}
if (typeof value === "number") {
console.log(value.toFixed(2)); // OK
}
unknown -- це безпечна альтернатива any. TypeScript змушує тебе перевірити тип перед використанням.
never -- тип "ніколи"
never означає значення, яке ніколи не може існувати:
// Функція, яка ніколи не завершується
function throwError(message: string): never {
throw new Error(message);
}
// Функція з нескінченним циклом
function infiniteLoop(): never {
while (true) {
// ...
}
}
// never в exhaustive check
type Shape = "circle" | "square" | "triangle";
function getArea(shape: Shape): number {
switch (shape) {
case "circle": return Math.PI * 10;
case "square": return 100;
case "triangle": return 50;
default:
// Якщо ми забудемо один з варіантів,
// TypeScript покаже помилку тут
const _exhaustive: never = shape;
return _exhaustive;
}
}
Type Narrowing -- звуження типів
Type narrowing -- це коли TypeScript звужує union type до конкретного типу на основі перевірок:
typeof
function process(value: string | number | boolean): string {
if (typeof value === "string") {
return value.toUpperCase(); // value: string
}
if (typeof value === "number") {
return value.toFixed(2); // value: number
}
return value ? "так" : "ні"; // value: boolean
}
Оператор in
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird): void {
if ("swim" in animal) {
animal.swim(); // animal: Fish
} else {
animal.fly(); // animal: Bird
}
}
instanceof
function formatDate(value: string | Date): string {
if (value instanceof Date) {
return value.toLocaleDateString("uk-UA"); // value: Date
}
return value; // value: string
}
Truthiness narrowing
function printName(name: string | null | undefined): void {
if (name) {
console.log(name.toUpperCase()); // name: string
} else {
console.log("Ім'я не вказано");
}
}
Підсумок
- Union types (
A | B) -- значення може бути одним з кількох типів - Literal types (
"admin" | "user") -- значення обмежене конкретними варіантами - Type aliases (
type Name = ...) -- іменовані типи для повторного використання - Tuples (
[string, number]) -- масив з фіксованими типами на кожній позиції - Enums -- набір іменованих констант (але часто краще union literal types)
- any -- вимикає перевірку типів (уникай!)
- unknown -- безпечна альтернатива
any(потрібна перевірка перед використанням) - never -- тип для значень, яких не може існувати
- Type narrowing -- звуження типу через
typeof,in,instanceof, truthiness
Що далі?
У наступному уроці ми дізнаємось про інтерфейси -- потужний спосіб описувати структуру об'єктів. Ти навчишся використовувати interface, optional та readonly модифікатори, а також зрозумієш, коли обирати interface, а коли type.
Корисні посилання:
- TypeScript Handbook: Everyday Types -- офіційна документація по типах
- TypeScript Handbook: Narrowing -- повний гайд по type narrowing
- TypeScript Playground -- експериментуй з union types, enums, unknown
- Matt Pocock: unknown vs any -- чому unknown краще за any