Вивчай

Generics

TypeScript Generics — ряд однакових скляних банок на полиці кожна з різним вмістом та підписомTypeScript Generics — ряд однакових скляних банок на полиці кожна з різним вмістом та підписом

Уяви, що ти пишеш функцію, яка повертає перший елемент масиву. Вона має працювати з масивом чисел, рядків, об'єктів -- будь-чого. Як типізувати таку функцію? Використати any? Ні, бо втратиш type safety. Використати overloading для кожного типу? Занадто багато коду. Рішення -- Generics.


Навіщо потрібні Generics?

Проблема без generics

// Варіант 1: any (погано -- втрачаємо типізацію)
function getFirst(arr: any[]): any {
  return arr[0];
}

const first = getFirst([1, 2, 3]); // first: any -- TypeScript не знає тип!

// Варіант 2: overloading (занадто багато коду)
function getFirst(arr: number[]): number;
function getFirst(arr: string[]): string;
function getFirst(arr: any[]): any {
  return arr[0];
}
// А якщо потрібен масив boolean? Об'єктів? Знову додавати overload?

Рішення -- Generic

// T -- "заповнювач типу", який буде визначений при виклику
function getFirst<T>(arr: T[]): T {
  return arr[0];
}

const num = getFirst([1, 2, 3]);           // num: number
const str = getFirst(["hello", "world"]);  // str: string
const bool = getFirst([true, false]);      // bool: boolean

// TypeScript визначає T автоматично з аргументів!

<T> -- це параметр типу (type parameter). Він працює як параметр функції, але для типів.


Generic Functions

Синтаксис

// function declaration
function identity<T>(value: T): T {
  return value;
}

// arrow function
const identity2 = <T>(value: T): T => value;

// Виклик -- TypeScript виводить T автоматично
identity(42);       // T = number
identity("hello");  // T = string

// Або можна вказати явно
identity<string>("hello"); // T = string явно

Кілька параметрів типу

function pair<A, B>(first: A, second: B): [A, B] {
  return [first, second];
}

const result = pair("Олексій", 25); // [string, number]

// Функція map
function mapArray<T, U>(arr: T[], fn: (item: T) => U): U[] {
  return arr.map(fn);
}

const numbers = [1, 2, 3];
const strings = mapArray(numbers, (n) => n.toString());
// strings: string[]

const lengths = mapArray(["hi", "hello"], (s) => s.length);
// lengths: number[]

Generic Interfaces

// Generic interface для API response
interface ApiResponse<T> {
  data: T;
  success: boolean;
  timestamp: string;
}

// Конкретні типи при використанні
interface User {
  name: string;
  email: string;
}

interface Product {
  id: number;
  title: string;
  price: number;
}

const userResponse: ApiResponse<User> = {
  data: { name: "Олексій", email: "alex@test.com" },
  success: true,
  timestamp: "2026-02-09T10:00:00",
};

const productsResponse: ApiResponse<Product[]> = {
  data: [
    { id: 1, title: "Ноутбук", price: 25000 },
    { id: 2, title: "Телефон", price: 15000 },
  ],
  success: true,
  timestamp: "2026-02-09T10:00:00",
};

Generic interface для колекції

interface Collection<T> {
  items: T[];
  add(item: T): void;
  remove(index: number): T | undefined;
  find(predicate: (item: T) => boolean): T | undefined;
  getAll(): T[];
}

// Реалізація
function createCollection<T>(initial: T[] = []): Collection<T> {
  const items: T[] = [...initial];

  return {
    items,
    add(item) {
      items.push(item);
    },
    remove(index) {
      return items.splice(index, 1)[0];
    },
    find(predicate) {
      return items.find(predicate);
    },
    getAll() {
      return [...items];
    },
  };
}

// Використання
const users = createCollection<User>([
  { name: "Олексій", email: "alex@test.com" },
]);

users.add({ name: "Марія", email: "maria@test.com" });
const found = users.find((u) => u.name === "Марія");
// found: User | undefined

Generic Type Aliases

// Nullable тип
type Nullable<T> = T | null;

let userName: Nullable<string> = "Олексій";
userName = null; // OK

// Result тип (успіх або помилка)
type Result<T, E = string> =
  | { success: true; data: T }
  | { success: false; error: E };

function divide(a: number, b: number): Result<number> {
  if (b === 0) {
    return { success: false, error: "Ділення на нуль" };
  }
  return { success: true, data: a / b };
}

const result = divide(10, 3);
if (result.success) {
  console.log(result.data); // number -- TypeScript знає!
} else {
  console.log(result.error); // string
}

Constraints -- обмеження generics

Іноді потрібно обмежити, які типи можна передати в generic. Для цього використовується extends:

// T повинен мати властивість length
function logLength<T extends { length: number }>(value: T): void {
  console.log(`Довжина: ${value.length}`);
}

logLength("hello");      // OK: string має length
logLength([1, 2, 3]);   // OK: array має length
logLength({ length: 5 }); // OK: об'єкт має length
// logLength(42);        // Помилка: number не має length

Constraint з keyof

// K повинен бути ключем об'єкта T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Олексій", age: 25, email: "alex@test.com" };

getProperty(user, "name");  // string
getProperty(user, "age");   // number
// getProperty(user, "phone"); // Помилка: "phone" не є ключем user

keyof T повертає union type всіх ключів об'єкта T. У прикладі вище keyof typeof user -- це "name" | "age" | "email".

Constraint extends interface

interface HasId {
  id: number;
}

function findById<T extends HasId>(items: T[], id: number): T | undefined {
  return items.find((item) => item.id === id);
}

const products = [
  { id: 1, title: "Ноутбук", price: 25000 },
  { id: 2, title: "Телефон", price: 15000 },
];

const found = findById(products, 1);
// found: { id: number; title: string; price: number } | undefined

Default Type Parameters

Generics можуть мати значення за замовчуванням:

interface ApiResponse<T = unknown> {
  data: T;
  status: number;
}

// Можна не вказувати T
const response: ApiResponse = {
  data: "something",
  status: 200,
};

// Або вказати явно
const userResponse: ApiResponse<User> = {
  data: { name: "Олексій", email: "alex@test.com" },
  status: 200,
};

Utility Types -- вбудовані generics

TypeScript має набір вбудованих generic типів, які вирішують типові задачі:

Partial<T> -- всі поля стають optional

interface User {
  name: string;
  email: string;
  age: number;
}

// Partial<User> = { name?: string; email?: string; age?: number }
function updateUser(user: User, updates: Partial<User>): User {
  return { ...user, ...updates };
}

const user: User = { name: "Олексій", email: "alex@test.com", age: 25 };
const updated = updateUser(user, { age: 26 }); // Тільки age

Required<T> -- всі поля стають обов'язковими

interface Config {
  host?: string;
  port?: number;
  debug?: boolean;
}

// Required<Config> = { host: string; port: number; debug: boolean }
const fullConfig: Required<Config> = {
  host: "localhost",
  port: 3000,
  debug: true,
};

Pick<T, K> -- обрати тільки вказані поля

interface User {
  name: string;
  email: string;
  age: number;
  password: string;
}

// Тільки name та email
type PublicUser = Pick<User, "name" | "email">;
// { name: string; email: string }

const publicUser: PublicUser = {
  name: "Олексій",
  email: "alex@test.com",
};

Omit<T, K> -- виключити вказані поля

// Все крім password
type SafeUser = Omit<User, "password">;
// { name: string; email: string; age: number }

// Omit -- протилежність Pick

Record<K, V> -- об'єкт з ключами K та значеннями V

// Словник переклади
type Language = "uk" | "en" | "de";

const greetings: Record<Language, string> = {
  uk: "Привіт",
  en: "Hello",
  de: "Hallo",
};

// Record для scores
type Subject = "math" | "english" | "physics";
const scores: Record<Subject, number> = {
  math: 95,
  english: 87,
  physics: 92,
};

Readonly<T> -- всі поля стають readonly

interface User {
  name: string;
  email: string;
}

const frozenUser: Readonly<User> = {
  name: "Олексій",
  email: "alex@test.com",
};

// frozenUser.name = "Марія"; // Помилка: Cannot assign to 'name'
Порада

Ці utility types можна комбінувати: Partial<Pick<User, "name" | "email">> -- optional name та email. Це дуже зручно для CRUD-операцій: Create потребує Omit<User, "id">, Update -- Partial<Omit<User, "id">>.


Практика: generic storage

interface StorageService<T extends { id: string | number }> {
  getAll(): T[];
  getById(id: T["id"]): T | undefined;
  add(item: T): void;
  update(id: T["id"], updates: Partial<Omit<T, "id">>): T | undefined;
  remove(id: T["id"]): boolean;
}

function createStorage<T extends { id: string | number }>(
  initial: T[] = []
): StorageService<T> {
  const items: T[] = [...initial];

  return {
    getAll: () => [...items],

    getById: (id) => items.find((item) => item.id === id),

    add: (item) => {
      items.push(item);
    },

    update: (id, updates) => {
      const index = items.findIndex((item) => item.id === id);
      if (index === -1) return undefined;
      items[index] = { ...items[index], ...updates };
      return items[index];
    },

    remove: (id) => {
      const index = items.findIndex((item) => item.id === id);
      if (index === -1) return false;
      items.splice(index, 1);
      return true;
    },
  };
}

// Використання з різними типами
interface Product {
  id: number;
  title: string;
  price: number;
}

const productStorage = createStorage<Product>([
  { id: 1, title: "Ноутбук", price: 25000 },
]);

productStorage.add({ id: 2, title: "Телефон", price: 15000 });
productStorage.update(1, { price: 23000 });
const product = productStorage.getById(1);
// product: Product | undefined

Підсумок

  • Generics (<T>) -- параметри типів, які визначаються при використанні
  • Generic functions: function fn<T>(arg: T): T
  • Generic interfaces: interface Box<T> { value: T }
  • Generic type aliases: type Result<T> = { data: T }
  • Constraints (extends) -- обмежують допустимі типи для generic
  • keyof -- отримує union ключів об'єкта
  • Default types: <T = string> -- значення за замовчуванням
  • Utility types: Partial, Required, Pick, Omit, Record, Readonly

Що далі?

В останньому уроці блоку ми навчимось використовувати TypeScript з DOM -- типізувати HTML-елементи, писати type guards, робити type assertions та типізувати fetch-запити. Це підготує тебе до роботи з React у Block 8.

Інфо

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