Вивчай

Types поглиблено

TypeScript 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.

Інфо

Корисні посилання: