#14 - Утилітарні типи в TypeScript

Утилітарні типи (Utility Types) — це вбудовані типи TypeScript, які дозволяють змінювати або маніпулювати існуючими типами. Вони допомагають зробити код більш гнучким і зрозумілим.

Сьогодні ми розберемо абсотно всі такі типи, включно з непопулярними, щоб ви не лякалися, коли бачили їх в коді якогось сніча. Деякі з них ми вже обговорювали в попередньому уроці, але я все одно перечислю їх всі.

Partial<T>

Робить всі властивості типу T необов’язковими

type User = {
  name: string;
  age: number;
};

const updateUser = (user: Partial<User>) => {
  // можна передавати частковий об'єкт
  console.log(user);
};

updateUser({ name: "Andrii" });

Required<T>

Робить всі властивості обов’язковими

type User = {
  name?: string;
  age?: number;
};

const createUser = (user: Required<User>) => {
  // тепер name та age — обов’язкові
};

createUser({ name: "Olena", age: 25 });

Readonly<T>

Робить всі властивості тільки для читання

type User = {
  name: string;
};

const user: Readonly<User> = {
  name: "Oleh"
};

// user.name = "Ivan"; // помилка: неможливо змінити readonly властивість

Pick<T, K>

Обирає лише вказані ключі з типу T

type User = {
  id: number;
  name: string;
  email: string;
};

type UserPreview = Pick<User, "id" | "name">;

Omit<T, K>

Видаляє вказані ключі з типу T

type User = {
  id: number;
  name: string;
  email: string;
};

type WithoutEmail = Omit<User, "email">;

Record<K, T>

Створює об’єкт з ключами K та значеннями типу T

type Roles = "admin" | "user" | "guest";
type Permissions = "read" | "write" | "delete";

const rolePermissions: Record<Roles, Permissions[]> = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"]
};

Exclude<T, U>

Видаляє з T всі типи, які є в U

type Status = "pending" | "approved" | "rejected";
type FinalStatus = Exclude<Status, "pending">;
// FinalStatus: "approved" | "rejected"

Extract<T, U>

Залишає в T лише ті типи, які є в U

type A = "a" | "b" | "c";
type B = "a" | "d";

type Common = Extract<A, B>; // "a"

ReturnType<T>

Отримує тип, який повертає функція T

function getUser() {
  return {
    name: "Sofiia",
    age: 30
  };
}

type User = ReturnType<typeof getUser>;

NonNullable<T>

Видаляє null та undefined з типу T

type MaybeName = string | null | undefined;
type CleanName = NonNullable<MaybeName>; // string

InstanceType<T>

Отримує тип екземпляру класу

class User {
  name = "Admin";
  login() {}
}

type UserInstance = InstanceType<typeof User>;
// UserInstance: User

const user: UserInstance = new User();

ThisType<T>

Дає контроль над контекстом this (використовується з об’єктами) 

Працює лише в поєднанні з noImplicitThis: true у tsconfig.

type ObjectWithHelpers = {
  x: number;
  y: number;
  helpers: {
    move(dx: number, dy: number): void;
  } & ThisType<{ x: number; y: number }>; // Тип для this у helpers
};

const point: ObjectWithHelpers = {
  x: 0,
  y: 0,
  helpers: {
    move(dx, dy) {
      this.x += dx;
      this.y += dy;
    },
  },
};

point.helpers.move(5, 7);
console.log(point.x, point.y); // 5 7

Awaited<T>

Отримує тип, який повертається з Promise<T> або async функції

type MaybePromise = Promise<string>;
type Result = Awaited<MaybePromise>; // string

async function fetchData() {
  return { id: 1, name: "Product" };
}

type Data = Awaited<ReturnType<typeof fetchData>>;
// Data: { id: number, name: string }

ConstructorParameters<T>

Працює як з класами, так і з конструкторами - отримує параметри конструктора класу.

class Person {
  constructor(public name: string, public age: number) {}
}

type PersonArgs = ConstructorParameters<typeof Person>;
// PersonArgs: [name: string, age: number]

const person = new Person(...["Anna", 22] satisfies PersonArgs);

AbstractConstructor<T>

Окремо хотів би ще показати як працювати з абстрактними класами на прикладі AbstractConstructor<T>, але його нема в стандартному пакеті утилітарних типів, тобто вам треба його створювати вручну, але він дуже користний для типізації абстрактних класів.

type AbstractConstructor<T> = abstract new (...args: any[]) => T;

abstract class Animal {
  abstract speak(): void;
}

class Dog extends Animal {
  speak() {
    console.log("Bark");
  }
}

function createInstance<C extends AbstractConstructor<Animal>>(ctor: C) {
  return new ctor();
}

const dog = createInstance(Dog); // ✅ OK