Вивчай
TypeScript

TypeScript шпаргалка — типи, інтерфейси, generics

Довідник з основ TypeScript для тих, хто вже знає JavaScript. Усі приклади — практичні, з реальними сценаріями. Для глибшого вивчення — уроки TypeScript. Для порівняння JS і TS — стаття TypeScript vs JavaScript.

Також дивись: JavaScript основи · JS масиви та методи


Базові типи

let name: string = "Олена";
let age: number = 25;
let isStudent: boolean = true;
let nothing: null = null;
let notDefined: undefined = undefined;
ТипОписПриклад
stringРядок"hello", `шаблон`
numberЧисло (int і float)42, 3.14
booleanЛогічнийtrue, false
nullЯвна відсутністьnull
undefinedНе визначеноundefined
anyБудь-що (вимикає перевірку)уникай!
unknownБудь-що (безпечний)потребує перевірки перед використанням
neverНіколи не відбуваєтьсяфункція, що завжди кидає помилку
voidНічого не повертаєфункція без return
Увага

any вимикає всю перевірку типів. Використовуй unknown замість any — він змусить перевірити тип перед використанням.


Type Inference — TypeScript вгадує типи

Не обов'язково писати тип скрізь — TS вміє виводити його автоматично:

let city = "Київ";          // TS знає: string
let count = 0;               // TS знає: number
let items = ["a", "b"];      // TS знає: string[]

const API_URL = "https://api.example.com"; // TS знає: "https://api.example.com" (literal type)

Правило: якщо TS може вивести тип — не пиши його вручну. Анотуй явно параметри функцій і складні випадки.


Функції

Типізація параметрів і повернення

function add(a: number, b: number): number {
  return a + b;
}

const greet = (name: string): string => {
  return `Привіт, ${name}!`;
};

Optional і default параметри

function createUser(name: string, age?: number): string {
  return age ? `${name}, ${age} років` : name;
}

function fetchData(url: string, timeout: number = 5000): void {
  // ...
}

Callback-функції

function processItems(items: string[], callback: (item: string, index: number) => void): void {
  items.forEach(callback);
}

Функція, що нічого не повертає vs яка ніколи не завершується

function logMessage(msg: string): void {
  console.log(msg);
}

function throwError(msg: string): never {
  throw new Error(msg);
}

Інтерфейси (interface)

Базовий інтерфейс

interface User {
  id: number;
  name: string;
  email: string;
  age?: number;              // optional — може бути, може ні
  readonly createdAt: Date;  // не можна змінити після створення
}

const user: User = {
  id: 1,
  name: "Олена",
  email: "olena@example.com",
  createdAt: new Date(),
};

Розширення (extends)

interface Admin extends User {
  role: "admin" | "superadmin";
  permissions: string[];
}

Інтерфейс для функції

interface SearchFunction {
  (query: string, limit?: number): Promise<string[]>;
}

Type Alias (type)

type ID = string | number;
type Status = "loading" | "success" | "error";
type Point = { x: number; y: number };

interface vs type — коли що

СитуаціяРекомендація
Об'єкт / класinterface
Union, literal, tupletype
Розширення (extends)обидва працюють
Declaration mergingтільки interface
// interface — розширюється через extends
interface Animal {
  name: string;
}
interface Dog extends Animal {
  breed: string;
}

// type — комбінується через &
type Animal2 = { name: string };
type Dog2 = Animal2 & { breed: string };

Union та Literal типи

Union — "або"

let id: string | number;
id = "abc";  // ✅
id = 123;    // ✅
id = true;   // ❌

Literal — конкретні значення

type Direction = "up" | "down" | "left" | "right";
type HttpStatus = 200 | 301 | 404 | 500;

function move(direction: Direction): void {
  // direction може бути ТІЛЬКИ "up", "down", "left", "right"
}

Discriminated Unions — для моделювання станів

type ApiResponse =
  | { status: "loading" }
  | { status: "success"; data: string[] }
  | { status: "error"; message: string };

function handle(response: ApiResponse) {
  switch (response.status) {
    case "loading":
      console.log("Завантаження...");
      break;
    case "success":
      console.log(response.data);   // TS знає: тут є .data
      break;
    case "error":
      console.log(response.message); // TS знає: тут є .message
      break;
  }
}

Масиви та кортежі

Масиви

const names: string[] = ["Олена", "Іван"];
const scores: Array<number> = [95, 87, 73];
const matrix: number[][] = [[1, 2], [3, 4]];

Кортежі (Tuple) — фіксована довжина і типи

const pair: [string, number] = ["вік", 25];
const rgb: [number, number, number] = [255, 128, 0];

// Деструктуризація
const [label, value] = pair; // label: string, value: number

Generics — узагальнені типи

Навіщо

Generics дозволяють писати код, який працює з будь-яким типом, зберігаючи перевірку:

// Без generics — втрачаємо тип
function first(arr: any[]): any {
  return arr[0];
}

// З generics — тип зберігається
function first<T>(arr: T[]): T {
  return arr[0];
}

first([1, 2, 3]);       // повертає number
first(["a", "b", "c"]); // повертає string

Generic інтерфейс

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

type UserResponse = ApiResponse<User>;
type ProductResponse = ApiResponse<Product[]>;

Constraints — обмеження generic

function getLength<T extends { length: number }>(item: T): number {
  return item.length;
}

getLength("hello");    // ✅ string має .length
getLength([1, 2, 3]);  // ✅ масив має .length
getLength(42);          // ❌ number не має .length

Utility Types — вбудовані трансформації

UtilityЩо робитьПриклад
Partial<T>Всі поля optionalPartial<User> — для форм оновлення
Required<T>Всі поля обов'язковіRequired<Config>
Pick<T, K>Обрати лише деякі поляPick<User, "id" | "name">
Omit<T, K>Прибрати деякі поляOmit<User, "password">
Record<K, V>Об'єкт з ключами K і значеннями VRecord<string, number>
Readonly<T>Всі поля readonlyReadonly<Config>

Приклади

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

// Для форми оновлення — не всі поля обов'язкові
type UpdateUser = Partial<User>;

// Для відображення — без пароля
type PublicUser = Omit<User, "password">;

// Для списку — тільки id і name
type UserPreview = Pick<User, "id" | "name">;

// Словник з рядковими ключами
type Scores = Record<string, number>;
const exam: Scores = { math: 95, english: 87 };

Type Narrowing — звуження типів

TypeScript звужує типи автоматично після перевірок:

typeof

function format(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase(); // TS знає: string
  }
  return value.toFixed(2);      // TS знає: number
}

instanceof

function handleError(error: Error | string): string {
  if (error instanceof Error) {
    return error.message; // TS знає: Error
  }
  return error;           // TS знає: string
}

in

interface Cat { meow(): void }
interface Dog { bark(): void }

function speak(animal: Cat | Dog) {
  if ("meow" in animal) {
    animal.meow(); // TS знає: Cat
  } else {
    animal.bark(); // TS знає: Dog
  }
}

Truthiness

function greet(name?: string) {
  if (name) {
    console.log(name.toUpperCase()); // TS знає: string (не undefined)
  }
}

Enums — і чому їх краще уникати

TypeScript має enum — але це одна з найспірніших його фіч. На відміну від усього іншого в TS, enum генерує реальний JavaScript-код у рантаймі. Це напівтип-напіврантайм конструкція, і її поведінка не завжди очевидна.

Як виглядає enum

enum Role {
  Admin = "ADMIN",
  Editor = "EDITOR",
  Viewer = "VIEWER",
}

Після компіляції це перетворюється на:

var Role;
(function (Role) {
  Role["Admin"] = "ADMIN";
  Role["Editor"] = "EDITOR";
  Role["Viewer"] = "VIEWER";
})(Role || (Role = {}));

Це зайвий код, який потрапляє в бандл. А числові enum ще гірші — вони створюють зворотний маппінг (ключ → значення І значення → ключ), що часто плутає.

Увага

Рекомендація: уникай enum. Використовуй as const + typeof — це чистий TypeScript без рантайм-артефактів, з кращим tree-shaking і більш передбачуваною поведінкою.

Краща альтернатива: as const + typeof

as const перетворює значення в точні літеральні типи — без генерації зайвого JS:

// ✅ Замість enum — об'єкт з as const
const ROLES = {
  Admin: "ADMIN",
  Editor: "EDITOR",
  Viewer: "VIEWER",
} as const;

type Role = (typeof ROLES)[keyof typeof ROLES]; // "ADMIN" | "EDITOR" | "VIEWER"

function checkAccess(role: Role): boolean {
  return role === ROLES.Admin;
}

Після компіляції — просто об'єкт. Ніяких IIFE, ніяких зворотних маппінгів. Тип Role існує тільки на етапі компіляції і повністю зникає з бандла.

as const для масивів і конфігів

// Масив з точними типами
const STATUSES = ["draft", "published", "archived"] as const;
type Status = (typeof STATUSES)[number]; // "draft" | "published" | "archived"

// Конфіг з точними значеннями
const config = {
  api: "https://api.example.com",
  timeout: 5000,
  retries: 3,
} as const;
// config.api має тип "https://api.example.com", а не string
// config.timeout має тип 5000, а не number

Простий випадок — union literal

Якщо не потрібен маппінг ключ → значення, найпростіший варіант:

type Direction = "up" | "down" | "left" | "right";
type HttpStatus = 200 | 301 | 404 | 500;
ПідхідРантайм-кодTree-shakingЗрозумілість
enumТак (IIFE)ПоганоНеочевидна поведінка
as const + typeofТільки об'єктДобреПрозоро
Union literalНіІдеальноНайпростіше

Best Practices — як писати кращий TypeScript

Думай інтерфейсами, не змінними

Перш ніж писати функцію — опиши shape даних. Це змінює підхід до дизайну коду:

// ❌ Спочатку код, потім типи (JS-мислення)
function createOrder(userId, items, discount) { ... }

// ✅ Спочатку контракт, потім код (TS-мислення)
interface CreateOrderInput {
  userId: string;
  items: OrderItem[];
  discount?: Discount;
}

function createOrder(input: CreateOrderInput): Order { ... }

Уникай type assertions (as) де можливо

as — це "повір мені, я знаю краще". Іноді потрібно, але частіше — ознака проблеми:

// ❌ Каже "це User" — але TS не перевіряє
const user = JSON.parse(data) as User;

// ✅ Перевіряє в рантаймі — TS звужує тип автоматично
function isUser(data: unknown): data is User {
  return typeof data === "object" && data !== null && "id" in data && "name" in data;
}

const parsed = JSON.parse(data);
if (isUser(parsed)) {
  console.log(parsed.name); // TS знає: це User
}

satisfies — перевір тип без втрати точності

type Theme = Record<string, string>;

// ❌ as Theme — втрачаємо точні ключі
const theme = { primary: "#007bff", secondary: "#6c757d" } as Theme;

// ✅ satisfies — перевіряє тип І зберігає точні ключі
const theme = {
  primary: "#007bff",
  secondary: "#6c757d",
} satisfies Theme;

theme.primary;   // ✅ TS знає що це конкретно "primary", не просто string
theme.tertiary;  // ❌ Property 'tertiary' does not exist

Використовуй strict mode

У tsconfig.json завжди вмикай "strict": true — це включає strictNullChecks, noImplicitAny та інші перевірки, які ловлять більшість помилок:

{
  "compilerOptions": {
    "strict": true
  }
}

Типові помилки

1. Використання any замість конкретного типу

// ❌ Погано — any вимикає перевірку
function parse(data: any) { return data.name; }

// ✅ Добре — конкретний тип або unknown
function parse(data: unknown) {
  if (typeof data === "object" && data !== null && "name" in data) {
    return (data as { name: string }).name;
  }
}

2. Зайві анотації де TS і так знає тип

// ❌ Зайве — TS виведе тип сам
const name: string = "Олена";
const items: number[] = [1, 2, 3];

// ✅ Достатньо
const name = "Олена";
const items = [1, 2, 3];

3. Забути обробити всі варіанти union

type Status = "active" | "inactive" | "banned";

// ❌ Забули "banned"
function getMessage(status: Status): string {
  if (status === "active") return "Активний";
  if (status === "inactive") return "Неактивний";
  // TS не попередить без exhaustive check
}

// ✅ Exhaustive check з never
function getMessage(status: Status): string {
  switch (status) {
    case "active": return "Активний";
    case "inactive": return "Неактивний";
    case "banned": return "Заблокований";
    default:
      const _exhaustive: never = status; // ❌ якщо забув варіант
      return _exhaustive;
  }
}

4. Мутація readonly поля

interface Config {
  readonly apiUrl: string;
}

const config: Config = { apiUrl: "https://api.example.com" };
config.apiUrl = "other"; // ❌ Cannot assign to 'apiUrl' because it is a read-only property

5. Плутати interface та type для union

// ❌ interface не може бути union
interface Status {
  "loading" | "success" | "error" // Syntax Error
}

// ✅ Для union — тільки type
type Status = "loading" | "success" | "error";

Інфо

Практика:

  • TypeScript Playground — експериментуй з типами прямо в браузері
  • Type Challenges — задачі на типи від простих до божевільних (44k+ зірок)

Документація:

React + TypeScript:

Matt Pocock (один з найкращих TS-авторів):

Наш курс: