Система типів мови TypeScript

Типізація функцій

Глибокий розбір типізації функцій у TypeScript: параметри, повернення значень, опціональні параметри, rest parameters, перевантаження функцій та callback типізація

Типізація функцій

🎯 Мета розділу

  • Зрозуміти, як функції зберігаються у JavaScript та чому вони є об'єктами першого класу.
  • Освоїти типізацію параметрів та повернення значень функцій.
  • Навчитися працювати з опціональними та default параметрами.
  • Опанувати rest parameters та spread оператор для змінної кількості аргументів.
  • Вивчити типізацію callback-функцій та функцій вищого порядку.
  • Усвідомити концепцію function overloading для складних сигнатур.
  • Розібратися з контекстом this та його типізацією.

🔑 Ключові терміни

  • Function Signature: сигнатура функції — типи параметрів та тип повернення.
  • Optional Parameter: опціональний параметр, що може бути відсутнім при виклику.
  • Default Parameter: параметр зі значенням за замовчуванням.
  • Rest Parameters: змінна кількість аргументів, зібраних у масив.
  • Callback: функція, передана як аргумент іншій функції.
  • Higher-Order Function: функція, що приймає або повертає інші функції.
  • Function Overloading: можливість визначити кілька сигнатур для однієї функції.

Функції як об'єкти першого класу

Функції у JavaScript: більше, ніж підпрограми

У JavaScript функції є об'єктами першого класу (first-class objects) — це означає, що з функціями можна робити все те саме, що й з іншими значеннями:

  • Зберігати у змінних
  • Передавати як аргументи іншим функціям
  • Повертати з функцій
  • Зберігати у масивах та об'єктах
  • Створювати динамічно під час виконання
// Функція як значення змінної
let greet = function(name) {
  return `Hello, ${name}!`;
};

// Функція як елемент масиву
let operations = [
  function(a, b) { return a + b; },
  function(a, b) { return a - b; },
  function(a, b) { return a * b; }
];

// Функція як властивість об'єкта
let calculator = {
  add: function(a, b) { return a + b; },
  subtract: function(a, b) { return a - b; }
};

// Функція як аргумент (callback)
[1, 2, 3].map(function(n) { return n * 2; });

// Функція як повернення значення
function createMultiplier(factor) {
  return function(n) {
    return n * factor;
  };
}

Саме ця властивість робить JavaScript потужною мовою для функціонального програмування та асинхронної обробки (callbacks, promises).

Функції зберігаються у пам'яті як об'єкти

Коли ви оголошуєте функцію, JavaScript створює об'єкт-функцію у купі (heap), а змінна містить посилання на цей об'єкт:

function greet(name) {
  return `Hello, ${name}!`;
}

// greet — це посилання на об'єкт-функцію
console.log(typeof greet);  // "function"
console.log(greet instanceof Object);  // true (функція — це об'єкт!)

// Функції можуть мати властивості!
greet.version = "1.0.0";
console.log(greet.version);  // "1.0.0"

Це пояснює, чому функції можна передавати та повертати — ви працюєте з посиланнями, а не копіюєте код функції.

Історична деталь: У ранніх версіях JavaScript функції та об'єкти були різними сутностями. Проте Brendan Eich (створювач JavaScript) вирішив зробити функції спеціальним видом об'єктів (callable objects), що можна викликати через (). Це рішення зробило мову набагато гнучкішою.

Оголошення функцій у TypeScript

Function Declaration vs Function Expression

У JavaScript є кілька способів створити функцію:

1. Function Declaration (оголошення функції):

function add(a: number, b: number): number {
  return a + b;
}
  • Piднімається (hoisting) на початок області видимості
  • Доступна до рядка оголошення у коді

2. Function Expression (функція-вираз):

const add = function(a: number, b: number): number {
  return a + b;
};
  • Зберігається у змінній
  • Не піднімається (змінна add піднімається, але ініціалізується як undefined)

3. Arrow Function (стрілочна функція):

const add = (a: number, b: number): number => {
  return a + b;
};

// Або коротка форма (без фігурних дужок)
const add = (a: number, b: number): number => a + b;
  • Компактний синтаксис
  • Не має власного this (успадковує з контексту)

4. Method Shorthand (метод об'єкта):

const calculator = {
  add(a: number, b: number): number {
    return a + b;
  }
};

Усі ці способи мають схожу типізацію у TypeScript.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF

rectangle "Function Declaration\nfunction add(a, b) { }" as FD #DCFCE7 {
    note bottom
    ✓ Hoisting (піднімається)
    ✓ Має власний this
    ✓ Може бути named
    end note
}

rectangle "Function Expression\nconst add = function(a, b) { }" as FE #DBEAFE {
    note bottom
    ✗ Hoisting (не піднімається)
    ✓ Має власний this
    ✓ Може бути anonymous
    end note
}

rectangle "Arrow Function\nconst add = (a, b) => { }" as AF #FEF3C7 {
    note bottom
    ✗ Hoisting (не піднімається)
    ✗ Немає власного this
    ✓ Компактний синтаксис
    end note
}

rectangle "Method Shorthand\n{ add(a, b) { } }" as MS #FED7AA {
    note bottom
    ✓ Має власний this
    ✓ Зручний для методів об'єктів
    ✓ Коротший синтаксис
    end note
}

note right of FD
  Вибір залежить від контексту:
  - Declaration: топ-рівень, експорти
  - Expression: динамічне створення
  - Arrow: callbacks, this-less функції
  - Method: методи об'єктів/класів
end note
@enduml

Hoisting: чому declaration доступна раніше

Function declaration піднімається на початок області видимості:

console.log(add(2, 3));  // 5 — працює!

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

Function expression не піднімається (змінна піднімається, але як undefined):

console.log(add(2, 3));  // Error: Cannot access 'add' before initialization

const add = function(a: number, b: number): number {
  return a + b;
};
Використовуйте function declaration для функцій верхнього рівня у модулях (особливо для експортованих функцій). Використовуйте arrow functions для callbacks та функцій, що не потребують власного this.

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

Обов'язкові параметри

У TypeScript кожен параметр функції обов'язково має тип:

function greet(name: string): string {
  return `Hello, ${name}!`;
}

greet("Alice");      // OK
greet(123);          // Error: Argument of type 'number' is not assignable to parameter of type 'string'
greet();             // Error: Expected 1 arguments, but got 0
greet("Alice", 28);  // Error: Expected 1 arguments, but got 2

TypeScript строго перевіряє:

  • Типи аргументів (мають відповідати типам параметрів)
  • Кількість аргументів (має співпадати з кількістю параметрів)

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

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

function formatUser(firstName: string, lastName: string, age: number): string {
  return `${firstName} ${lastName} (${age})`;
}

console.log(add(5, 3));                          // 8
console.log(formatUser("Alice", "Smith", 28));  // "Alice Smith (28)"

Type Inference для параметрів у стрілочних функціях

Якщо стрілочна функція використовується у контексті, де TypeScript знає очікуваний тип, параметри можуть виводитися автоматично:

// TypeScript знає, що map очікує функцію (n: number) => number
let numbers = [1, 2, 3, 4, 5];

// Параметр n автоматично має тип number
let doubled = numbers.map(n => n * 2);
//                        ^
//                   TypeScript виводить: n: number

// Явна типізація (можна, але не обов'язково)
let tripled = numbers.map((n: number) => n * 3);

Для callback-функцій контекст часто достатній:

let names = ["Alice", "Bob", "Charlie"];

// name автоматично виводиться як string
names.forEach(name => {
  console.log(name.toUpperCase());  // OK: TypeScript знає, що name — string
});

// index та array теж виводяться автоматично
names.forEach((name, index, array) => {
  console.log(`${index}: ${name} (total: ${array.length})`);
  //           ^         ^                  ^
  //         number    string            number
});

Типізація повернення значень

Явна анотація типу повернення

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

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

function greet(name: string): string {
  return `Hello, ${name}!`;
}

function isEven(n: number): boolean {
  return n % 2 === 0;
}

TypeScript перевіряє, що повернене значення відповідає типу:

function add(a: number, b: number): number {
  return `${a + b}`;  // Error: Type 'string' is not assignable to type 'number'
}

function greet(name: string): string {
  // Error: A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value
}

Type Inference для типу повернення

TypeScript може автоматично вивести тип повернення на основі виразів return:

// TypeScript виводить: number
function add(a: number, b: number) {
  return a + b;
}

// TypeScript виводить: string
function greet(name: string) {
  return `Hello, ${name}!`;
}

// TypeScript виводить: number | string (union type)
function format(value: number, asString: boolean) {
  if (asString) {
    return String(value);  // string
  }
  return value;            // number
}

Чи варто явно вказувати тип повернення?

Аргументи "за" явну типізацію:

  • Документація: тип повернення одразу видно у сигнатурі
  • Захист від помилок: TypeScript перевіряє, що ви повертаєте правильний тип
  • Стабільність API: зміна реалізації не змінить тип повернення випадково

Аргументи "за" type inference:

  • Менше коду
  • TypeScript сам виводить тип точно
// ✅ Добре: явний тип для публічних функцій
export function calculateDiscount(price: number, percent: number): number {
  return price * (1 - percent / 100);
}

// ✅ Добре: inference для локальних хелперів
function formatPrice(price: number) {
  return `$${price.toFixed(2)}`;
}
Рекомендація: Завжди явно вказуйте тип повернення для:
  • Експортованих функцій (public API)
  • Методів класів
  • Складних функцій з кількома return
Для локальних хелперів та простих функцій можна покластися на type inference.

Void: функції без повернення значення

Функції, що не повертають значення (або повертають undefined), мають тип повернення void:

function logMessage(message: string): void {
  console.log(message);
  // немає return або return без значення
}

function greet(name: string): void {
  console.log(`Hello, ${name}!`);
  return;  // OK: return без значення
}

Void vs undefined:

// void: функція НЕ має повертати значення
function log1(message: string): void {
  console.log(message);
}

// undefined: функція ПОВИННА явно повернути undefined
function log2(message: string): undefined {
  console.log(message);
  return undefined;  // обов'язково!
}

У більшості випадків використовуйте void для функцій без повернення.

Never: функції, що ніколи не завершуються

Тип never для функцій, що ніколи не повертають керування:

  • Функції, що завжди викидають помилку
  • Функції з нескінченними циклами
function throwError(message: string): never {
  throw new Error(message);
  // після throw код не виконується — функція не повертає нічого
}

function infiniteLoop(): never {
  while (true) {
    console.log("Running...");
  }
  // цикл ніколи не завершиться — функція не поверне керування
}

Різниця між void та never:

  • void: функція виконується та завершується, але не повертає значення
  • never: функція ніколи не завершується нормально
function log(message: string): void {
  console.log(message);
  // функція завершується та повертає undefined
}

function crash(message: string): never {
  throw new Error(message);
  // функція ніколи не дійде до кінця
}

Опціональні параметри

Синтаксис опціональних параметрів

Параметр можна зробити опціональним через ?:

function greet(name: string, greeting?: string): string {
  if (greeting) {
    return `${greeting}, ${name}!`;
  }
  return `Hello, ${name}!`;
}

greet("Alice");                  // "Hello, Alice!"
greet("Bob", "Good morning");    // "Good morning, Bob!"

Опціональний параметр має тип T | undefined:

function formatUser(name: string, age?: number): string {
  // age має тип: number | undefined
  
  if (age !== undefined) {
    return `${name} (${age})`;
  }
  return name;
}

console.log(formatUser("Alice"));       // "Alice"
console.log(formatUser("Bob", 32));     // "Bob (32)"

Опціональні параметри мають бути після обов'язкових:

// ✅ Правильно: опціональні в кінці
function example1(required: string, optional?: number): void { }

// ❌ Помилка: опціональний параметр перед обов'язковим
function example2(optional?: number, required: string): void { }
// Error: A required parameter cannot follow an optional parameter

Опціональні параметри vs Union з undefined

Є дві схожі, але різні конструкції:

// Опціональний параметр: можна НЕ передавати
function greet1(name: string, greeting?: string): void {
  console.log(greeting || "Hello", name);
}

greet1("Alice");           // OK: greeting відсутній
greet1("Bob", "Hi");       // OK
greet1("Charlie", undefined); // OK: явний undefined

// Union з undefined: ОБОВ'ЯЗКОВИЙ, але може бути undefined
function greet2(name: string, greeting: string | undefined): void {
  console.log(greeting || "Hello", name);
}

greet2("Alice");           // Error: Expected 2 arguments, but got 1
greet2("Bob", "Hi");       // OK
greet2("Charlie", undefined); // OK: явний undefined

У більшості випадків використовуйте опціональні параметри (?) — вони зручніші.


Default Parameters (параметри за замовчуванням)

Синтаксис default параметрів

JavaScript (ES6+) та TypeScript підтримують параметри зі значеннями за замовчуванням:

function greet(name: string, greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}

console.log(greet("Alice"));                // "Hello, Alice!"
console.log(greet("Bob", "Good morning"));  // "Good morning, Bob!"

TypeScript автоматично виводить тип з default значення:

function multiply(a: number, b = 2): number {
  //                          ^
  //                   TypeScript виводить: b: number = 2
  return a * b;
}

console.log(multiply(5));      // 10 (5 * 2)
console.log(multiply(5, 3));   // 15 (5 * 3)

Default параметри vs опціональні параметри

Default параметри автоматично роблять параметр опціональним:

// Обидва підходи схожі:

// Опціональний параметр
function greet1(name: string, greeting?: string): string {
  return `${greeting || "Hello"}, ${name}!`;
}

// Default параметр
function greet2(name: string, greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}

Різниця:

  • Опціональний параметр має тип T | undefined — потрібна перевірка
  • Default параметр має тип T — значення гарантовано існує
function example1(value?: number): void {
  // value: number | undefined
  console.log(value.toFixed(2));  // Error: value може бути undefined
  
  if (value !== undefined) {
    console.log(value.toFixed(2));  // OK
  }
}

function example2(value: number = 0): void {
  // value: number (гарантовано існує)
  console.log(value.toFixed(2));  // OK: завжди number
}
Віддавайте перевагу default параметрам, якщо є логічне значення за замовчуванням. Використовуйте опціональні параметри, якщо відсутність значення має особливий сенс.

Default параметри можуть бути складними виразами

Default значення може бути будь-яким виразом, навіть викликом функції:

function generateId(): string {
  return `id_${Date.now()}_${Math.random().toString(36)}`;
}

function createUser(name: string, id: string = generateId()): { name: string; id: string } {
  return { name, id };
}

console.log(createUser("Alice"));
// { name: "Alice", id: "id_1725000000000_abc123" }

console.log(createUser("Bob", "custom_id"));
// { name: "Bob", id: "custom_id" }

Default вираз обчислюється при кожному виклику:

function log(message: string, timestamp: number = Date.now()): void {
  console.log(`[${timestamp}] ${message}`);
}

log("First call");   // [1725000000000] First call
// ... пауза ...
log("Second call");  // [1725000001000] Second call (новий timestamp!)

Rest Parameters (змінна кількість аргументів)

Синтаксис rest parameters

Rest parameters дозволяють функції приймати змінну кількість аргументів, зібраних у масив:

function sum(...numbers: number[]): number {
  return numbers.reduce((total, n) => total + n, 0);
}

console.log(sum(1, 2, 3));           // 6
console.log(sum(10, 20, 30, 40));    // 100
console.log(sum());                  // 0 (порожній масив)

Синтаксис: ...parameterName: Type[]

  • ... — spread/rest оператор
  • numbers — ім'я параметра (буде масивом)
  • number[] — тип елементів масиву

Rest параметр має бути останнім

// ✅ Правильно: rest параметр в кінці
function format(template: string, ...values: unknown[]): string {
  return template.replace(/{(\d+)}/g, (_, index) => String(values[index]));
}

console.log(format("Hello, {0}! You are {1} years old.", "Alice", 28));
// "Hello, Alice! You are 28 years old."

// ❌ Помилка: rest параметр не в кінці
function example(...numbers: number[], suffix: string): void { }
// Error: A rest parameter must be last in a parameter list

Комбінування обов'язкових, опціональних та rest параметрів

function logMessage(
  level: "info" | "warn" | "error",    // обов'язковий
  message: string,                      // обов'язковий
  context?: string,                     // опціональний
  ...details: unknown[]                 // rest параметр
): void {
  console.log(`[${level.toUpperCase()}] ${message}`);
  
  if (context) {
    console.log(`Context: ${context}`);
  }
  
  if (details.length > 0) {
    console.log("Details:", details);
  }
}

logMessage("info", "User logged in");
// [INFO] User logged in

logMessage("warn", "High memory usage", "server-01");
// [WARN] High memory usage
// Context: server-01

logMessage("error", "Database connection failed", "db-primary", { host: "localhost", port: 5432 });
// [ERROR] Database connection failed
// Context: db-primary
// Details: [{ host: "localhost", port: 5432 }]

Spread оператор для передачі масиву

Spread оператор (...) дозволяє розгорнути масив у окремі аргументи:

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

let numbers = [1, 2, 3];

// Без spread: передається масив як один аргумент
// add(numbers);  // Error: Expected 3 arguments, but got 1

// Зі spread: масив розгортається у 3 аргументи
console.log(add(...numbers));  // 6 (те саме, що add(1, 2, 3))

Spread для копіювання масивів:

let original = [1, 2, 3];
let copy = [...original];  // створює новий масив

copy.push(4);

console.log(original);  // [1, 2, 3]
console.log(copy);      // [1, 2, 3, 4]

Spread для об'єднання масивів:

let first = [1, 2, 3];
let second = [4, 5, 6];
let combined = [...first, ...second];  // [1, 2, 3, 4, 5, 6]
Rest vs Spread:
  • Rest (...numbers: number[]) — збирає кілька аргументів у один масив (у параметрах функції)
  • Spread (add(...numbers)) — розгортає масив у окремі аргументи (при виклику функції)
Це два боки однієї монети: rest збирає, spread розгортає.

Типізація callback-функцій

Callback як параметр

Callback — це функція, передана як аргумент іншій функції:

function processArray(
  numbers: number[],
  callback: (n: number) => number
): number[] {
  return numbers.map(callback);
}

// Передаємо callback як стрілочну функцію
let doubled = processArray([1, 2, 3], (n) => n * 2);
console.log(doubled);  // [2, 4, 6]

// Передаємо callback як function expression
let squared = processArray([1, 2, 3], function(n) { return n * n; });
console.log(squared);  // [1, 4, 9]

Синтаксис типу callback:

(параметри) => типПовернення

Приклади:

// Callback без параметрів
let onClick: () => void;

// Callback з одним параметром
let onInput: (value: string) => void;

// Callback з кількома параметрами
let onSubmit: (username: string, password: string) => boolean;

// Callback, що повертає значення
let transform: (value: number) => string;

Inline-типізація callback-функцій

Для простих callback-функцій типізацію можна писати inline:

function forEach(items: string[], callback: (item: string, index: number) => void): void {
  for (let i = 0; i < items.length; i++) {
    callback(items[i], i);
  }
}

forEach(["Alice", "Bob", "Charlie"], (name, index) => {
  console.log(`${index}: ${name}`);
});
// 0: Alice
// 1: Bob
// 2: Charlie

TypeScript виводить типи параметрів callback:

function map(items: number[], callback: (item: number) => number): number[] {
  let result: number[] = [];
  for (let item of items) {
    result.push(callback(item));
  }
  return result;
}

// TypeScript знає, що item — це number
let doubled = map([1, 2, 3], item => item * 2);
//                           ^
//                  TypeScript виводить: item: number

Опціональні callback

Callback можна зробити опціональним:

function fetchData(
  url: string,
  onSuccess?: (data: unknown) => void,
  onError?: (error: Error) => void
): void {
  fetch(url)
    .then(response => response.json())
    .then(data => {
      if (onSuccess) {
        onSuccess(data);  // викликаємо, якщо передано
      }
    })
    .catch(error => {
      if (onError) {
        onError(error);  // викликаємо, якщо передано
      }
    });
}

// Використання
fetchData("/api/users", data => {
  console.log("Data received:", data);
});

fetchData(
  "/api/users",
  data => console.log("Success:", data),
  error => console.error("Error:", error)
);

Callback з контекстом (this)

Якщо callback використовує this, потрібно явно типізувати контекст:

function addEventListener(
  event: string,
  callback: (this: HTMLElement, event: Event) => void
): void {
  // реалізація додавання слухача
}

addEventListener("click", function(event) {
  // Тут this: HTMLElement
  console.log(this.id);  // OK: TypeScript знає, що this — це HTMLElement
});

Синтаксис this: Type у параметрах — це псевдопараметр, що не передається при виклику, а лише типізує контекст.

Arrow functions не мають власного this: Якщо callback має бути стрілочною функцією, вона успадковує this з зовнішнього контексту і не може мати this-параметр:
addEventListener("click", (event) => {
  // this тут — НЕ HTMLElement, а контекст з місця визначення
  console.log(this);  // може бути window, undefined, або інший контекст
});
Використовуйте звичайні функції (function), якщо потрібен доступ до this.

Функції вищого порядку (Higher-Order Functions)

Функції, що повертають функції

Функція вищого порядку (higher-order function) — це функція, що приймає інші функції як аргументи або повертає функції як результат.

function createMultiplier(factor: number): (n: number) => number {
  return (n) => n * factor;
}

let double = createMultiplier(2);
let triple = createMultiplier(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

Типізація:

  • Тип повернення: (n: number) => number — функція, що приймає number і повертає number

Практичний приклад: фабрика валідаторів:

function createValidator(minLength: number): (value: string) => boolean {
  return (value) => value.length >= minLength;
}

let validateUsername = createValidator(3);
let validatePassword = createValidator(8);

console.log(validateUsername("Al"));          // false
console.log(validateUsername("Alice"));       // true
console.log(validatePassword("pass"));        // false
console.log(validatePassword("password123")); // true

Замикання (Closures)

Функції, що повертаються, зберігають доступ до змінних зовнішньої функції (замикання, closure):

function createCounter(start: number): () => number {
  let count = start;  // змінна зберігається у замиканні
  
  return () => {
    count++;
    return count;
  };
}

let counter1 = createCounter(0);
let counter2 = createCounter(100);

console.log(counter1());  // 1
console.log(counter1());  // 2
console.log(counter1());  // 3

console.log(counter2());  // 101
console.log(counter2());  // 102

Кожен виклик createCounter створює окреме замикання зі власною копією змінної count.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF

rectangle "createCounter(0)" as Create1 #DCFCE7 {
    rectangle "Замикання 1" as Closure1 #86EFAC {
        note bottom
        count = 0 (потім 1, 2, 3...)
        end note
    }
    rectangle "Функція counter1" as Func1 #BBF7D0
}

rectangle "createCounter(100)" as Create2 #DBEAFE {
    rectangle "Замикання 2" as Closure2 #93C5FD {
        note bottom
        count = 100 (потім 101, 102...)
        end note
    }
    rectangle "Функція counter2" as Func2 #BFDBFE
}

Func1 --> Closure1 : має доступ до
Func2 --> Closure2 : має доступ до

note right of Create1
  Кожне замикання —
  це окремий "контекст"
  зі власними змінними
end note
@enduml

Карирування (Currying)

Карирування — це техніка перетворення функції з кількома параметрами на послідовність функцій з одним параметром:

// Звичайна функція з 3 параметрами
function add(a: number, b: number, c: number): number {
  return a + b + c;
}

// Карирована версія
function addCurried(a: number): (b: number) => (c: number) => number {
  return (b) => (c) => a + b + c;
}

// Використання
let result1 = add(1, 2, 3);  // 6

let result2 = addCurried(1)(2)(3);  // 6

// Часткове застосування
let add1 = addCurried(1);      // (b: number) => (c: number) => number
let add1and2 = add1(2);        // (c: number) => number
let result3 = add1and2(3);     // 6

Карирування корисне для створення спеціалізованих функцій з попередньо заданими параметрами.


Function Overloading (перевантаження функцій)

Проблема: різні сигнатури для різних входів

Іноді функція має різну поведінку та різні типи повернення залежно від типів аргументів:

// Хочемо, щоб функція:
// - приймала string → повертала string
// - приймала number → повертала number
// - приймала string[] → повертала string[]

function process(input: string | number | string[]): string | number | string[] {
  if (typeof input === "string") {
    return input.toUpperCase();
  } else if (typeof input === "number") {
    return input * 2;
  } else {
    return input.map(s => s.toUpperCase());
  }
}

let result1 = process("hello");  // тип: string | number | string[]
let result2 = process(5);        // тип: string | number | string[]
let result3 = process(["a", "b"]); // тип: string | number | string[]

Проблема: TypeScript не знає точного типу повернення — він виводить широкий union type string | number | string[] для всіх випадків, хоча ми знаємо:

  • process(string)string
  • process(number)number
  • process(string[])string[]

Синтаксис Function Overloading

Function overloading дозволяє визначити кілька сигнатур для однієї функції:

// Overload signatures (сигнатури перевантаження)
function process(input: string): string;
function process(input: number): number;
function process(input: string[]): string[];

// Implementation signature (реалізація)
function process(input: string | number | string[]): string | number | string[] {
  if (typeof input === "string") {
    return input.toUpperCase();
  } else if (typeof input === "number") {
    return input * 2;
  } else {
    return input.map(s => s.toUpperCase());
  }
}

// Тепер TypeScript знає точні типи!
let result1 = process("hello");  // тип: string
let result2 = process(5);        // тип: number
let result3 = process(["a", "b"]); // тип: string[]

Структура:

  1. Overload signatures (декілька) — описують різні способи виклику функції
  2. Implementation signature (одна) — фактична реалізація, що має охоплювати всі overload signatures
Implementation signature не видна зовні: TypeScript використовує лише overload signatures для перевірки типів при виклику. Implementation signature має бути достатньо широкою, щоб охопити всі overload signatures.

Порядок overload signatures має значення

TypeScript перевіряє overload signatures зверху вниз і вибирає першу, що підходить:

// ❌ Неправильний порядок
function format(value: unknown): string;         // занадто широка, підходить для всього
function format(value: number): string;          // ніколи не викличеться!
function format(value: string): string;          // ніколи не викличеться!

function format(value: unknown): string {
  return String(value);
}

// ✅ Правильний порядок: від специфічних до загальних
function format(value: number): string;
function format(value: string): string;
function format(value: unknown): string;

function format(value: unknown): string {
  return String(value);
}

Правило: Розташовуйте більш специфічні сигнатури вище, більш загальні — нижче.

Приклад: функція createElement

// Створює DOM-елемент з різними сигнатурами

// Overload 1: створити елемент без атрибутів
function createElement(tag: string): HTMLElement;

// Overload 2: створити елемент з атрибутами
function createElement(tag: string, attributes: { [key: string]: string }): HTMLElement;

// Overload 3: створити елемент з атрибутами та дітьми
function createElement(
  tag: string,
  attributes: { [key: string]: string },
  children: HTMLElement[]
): HTMLElement;

// Implementation
function createElement(
  tag: string,
  attributes?: { [key: string]: string },
  children?: HTMLElement[]
): HTMLElement {
  let element = document.createElement(tag);
  
  if (attributes) {
    for (let key in attributes) {
      element.setAttribute(key, attributes[key]);
    }
  }
  
  if (children) {
    children.forEach(child => element.appendChild(child));
  }
  
  return element;
}

// Використання — TypeScript знає типи
let div1 = createElement("div");
let div2 = createElement("div", { class: "container" });
let div3 = createElement("div", { id: "main" }, [
  createElement("p"),
  createElement("span")
]);

Коли використовувати overloading

Використовуйте overloading, коли:

  • Тип повернення залежить від типу вхідних аргументів
  • Різні комбінації параметрів мають різне значення
  • Потрібна точна типізація для кожного випадку

НЕ використовуйте overloading, коли:

  • Опціональні параметри достатні
  • Union types адекватно описують всі випадки
  • Логіка функції не змінюється залежно від типів
// ❌ Погано: overloading для опціональних параметрів
function greet(name: string): string;
function greet(name: string, greeting: string): string;
function greet(name: string, greeting?: string): string {
  return `${greeting || "Hello"}, ${name}!`;
}

// ✅ Добре: просто опціональний параметр
function greet(name: string, greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}

Типізація контексту this

Проблема з this у JavaScript

У JavaScript значення this залежить від способу виклику функції, а не від місця її визначення:

let user = {
  name: "Alice",
  greet: function() {
    console.log(`Hello, ${this.name}!`);
  }
};

user.greet();  // "Hello, Alice!" (this = user)

let greetFunc = user.greet;
greetFunc();   // "Hello, undefined!" (this = undefined у strict mode або window)

TypeScript за замовчуванням має тип any для this, що небезпечно.

Типізація this через псевдопараметр

TypeScript дозволяє явно типізувати this через псевдопараметр (не передається при виклику):

function greet(this: { name: string }): void {
  console.log(`Hello, ${this.name}!`);
}

let user = { name: "Alice", greet: greet };

user.greet();  // OK: this = user, що має властивість name

greet();  // Error: The 'this' context of type 'void' is not assignable to method's 'this' of type '{ name: string }'

Синтаксис: this: Type як перший параметр (але не враховується у кількості аргументів).

Приклад: метод об'єкта з типізованим this

let calculator = {
  value: 0,
  
  add(this: { value: number }, n: number): void {
    this.value += n;
  },
  
  subtract(this: { value: number }, n: number): void {
    this.value -= n;
  },
  
  getValue(this: { value: number }): number {
    return this.value;
  }
};

calculator.add(5);
calculator.subtract(2);
console.log(calculator.getValue());  // 3

// Спроба викликати метод без правильного this
let add = calculator.add;
// add(5);  // Error: The 'this' context of type 'void' is not assignable...

Arrow functions і this

Arrow functions не мають власного this — вони успадковують this з місця визначення:

let user = {
  name: "Alice",
  
  // Звичайна функція: має власний this
  greet1: function() {
    console.log(`Hello, ${this.name}!`);
  },
  
  // Arrow function: успадковує this з зовнішнього контексту
  greet2: () => {
    console.log(`Hello, ${this.name}!`);
  }
};

user.greet1();  // "Hello, Alice!" (this = user)
user.greet2();  // "Hello, undefined!" (this = глобальний контекст)

Використовуйте arrow functions для callback, де потрібен стабільний this:

class Timer {
  seconds = 0;
  
  start(): void {
    // Arrow function зберігає this від класу
    setInterval(() => {
      this.seconds++;
      console.log(this.seconds);
    }, 1000);
    
    // Звичайна функція втратила б this
    // setInterval(function() {
    //   this.seconds++;  // Error: this тут undefined
    // }, 1000);
  }
}

let timer = new Timer();
timer.start();  // 1, 2, 3, ...
Правило вибору:
  • Методи об'єктів/класів: Використовуйте звичайні функції (method() { }) для доступу до this.
  • Callbacks, event handlers: Використовуйте arrow functions (() => { }) для збереження зовнішнього this.

Практичні приклади та патерни

Приклад 1: Функція сортування з порівнювачем

function sortBy<T>(
  array: T[],
  compareFn: (a: T, b: T) => number
): T[] {
  return [...array].sort(compareFn);
}

let numbers = [5, 2, 8, 1, 9];
let sorted = sortBy(numbers, (a, b) => a - b);
console.log(sorted);  // [1, 2, 5, 8, 9]

let users = [
  { name: "Alice", age: 28 },
  { name: "Bob", age: 32 },
  { name: "Charlie", age: 25 }
];

let sortedByAge = sortBy(users, (a, b) => a.age - b.age);
console.log(sortedByAge);
// [
//   { name: "Charlie", age: 25 },
//   { name: "Alice", age: 28 },
//   { name: "Bob", age: 32 }
// ]

Приклад 2: Retry-функція з експоненційним backoff

async function retry<T>(
  fn: () => Promise<T>,
  maxAttempts: number = 3,
  delayMs: number = 1000
): Promise<T> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxAttempts) {
        throw error;
      }
      
      // Експоненційний backoff: 1s, 2s, 4s, ...
      let delay = delayMs * Math.pow(2, attempt - 1);
      console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  throw new Error("Retry failed");
}

// Використання
async function fetchData(): Promise<string> {
  let response = await fetch("/api/data");
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.text();
}

retry(fetchData, 5, 500)
  .then(data => console.log("Data:", data))
  .catch(error => console.error("Failed after retries:", error));

Приклад 3: Debounce-функція

function debounce<T extends unknown[]>(
  fn: (...args: T) => void,
  delayMs: number
): (...args: T) => void {
  let timeoutId: number | undefined;
  
  return (...args: T) => {
    if (timeoutId !== undefined) {
      clearTimeout(timeoutId);
    }
    
    timeoutId = setTimeout(() => {
      fn(...args);
    }, delayMs);
  };
}

// Використання
function handleInput(value: string): void {
  console.log("Searching for:", value);
  // виконати запит до API
}

let debouncedInput = debounce(handleInput, 300);

// Виклики з затримкою менше 300ms будуть скасовані
debouncedInput("a");
debouncedInput("ab");
debouncedInput("abc");  // тільки цей виклик виконається через 300ms

Приклад 4: Pipe-функція для композиції

function pipe<A>(value: A): A;
function pipe<A, B>(value: A, fn1: (a: A) => B): B;
function pipe<A, B, C>(value: A, fn1: (a: A) => B, fn2: (b: B) => C): C;
function pipe<A, B, C, D>(
  value: A,
  fn1: (a: A) => B,
  fn2: (b: B) => C,
  fn3: (c: C) => D
): D;

function pipe(value: unknown, ...fns: ((arg: unknown) => unknown)[]): unknown {
  return fns.reduce((acc, fn) => fn(acc), value);
}

// Використання
let result = pipe(
  5,
  (n: number) => n * 2,      // 10
  (n: number) => n + 3,      // 13
  (n: number) => n.toString() // "13"
);

console.log(result);  // "13"

Підсумок

У цьому розділі ви опанували типізацію функцій у TypeScript:

Оголошення функцій:

  • Function declaration, function expression, arrow function, method shorthand
  • Hoisting: function declaration піднімається, expression — ні
  • Функції є об'єктами першого класу у JavaScript

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

  • Обов'язкові параметри з типами
  • Type inference для параметрів у контексті (callbacks)
  • Кожен параметр має тип, кількість аргументів перевіряється

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

  • Явна анотація типу повернення через : Type
  • Type inference для простих функцій
  • void для функцій без повернення, never для функцій, що ніколи не завершуються

Опціональні та default параметри:

  • Опціональний параметр через ?: param?: Type
  • Default параметр: param: Type = defaultValue
  • Опціональні параметри мають тип T | undefined, default — лише T

Rest parameters:

  • ...params: Type[] для змінної кількості аргументів
  • Rest параметр має бути останнім
  • Spread оператор ...array для розгортання масиву у аргументи

Callbacks та Higher-Order Functions:

  • Inline-типізація callbacks: (param: Type) => ReturnType
  • TypeScript виводить типи параметрів callback з контексту
  • Функції вищого порядку: приймають або повертають інші функції
  • Замикання (closures) зберігають доступ до зовнішніх змінних

Function Overloading:

  • Кілька overload signatures для опису різних способів виклику
  • Implementation signature охоплює всі overload signatures
  • Порядок має значення: від специфічних до загальних

Типізація this:

  • Псевдопараметр this: Type для явної типізації контексту
  • Arrow functions не мають власного this, успадковують з зовнішнього контексту
  • Використовуйте звичайні функції для методів, arrow functions для callbacks

Ключові рекомендації:

  • Завжди типізуйте публічні функції явно (параметри та повернення)
  • Використовуйте default параметри замість опціональних, якщо є логічне значення за замовчуванням
  • Віддавайте перевагу type inference для локальних функцій та callbacks
  • Використовуйте overloading для функцій з різними поведінками залежно від типів
  • Arrow functions для callbacks, звичайні функції для методів
Copyright © 2026