Типізація функцій
Типізація функцій
🎯 Мета розділу
- Зрозуміти, як функції зберігаються у 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"
Це пояснює, чому функції можна передавати та повертати — ви працюєте з посиланнями, а не копіюєте код функції.
(). Це рішення зробило мову набагато гнучкішою.Оголошення функцій у 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.
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;
};
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
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 значення може бути будь-яким виразом, навіть викликом функції:
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 (
...numbers: number[]) — збирає кілька аргументів у один масив (у параметрах функції) - Spread (
add(...numbers)) — розгортає масив у окремі аргументи (при виклику функції)
Типізація 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 у параметрах — це псевдопараметр, що не передається при виклику, а лише типізує контекст.
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.
Карирування (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)→stringprocess(number)→numberprocess(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[]
Структура:
- 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, звичайні функції для методів
Function Declaration:
function greet(name: string): string {
return `Hello, ${name}!`;
}
console.log(greet("Alice")); // OK: працює ДО оголошення!
- Hoisting: Піднімається на початок області видимості, доступна до рядка оголошення
- Використання: Функції верхнього рівня, експорти
Function Expression:
const greet = function(name: string): string {
return `Hello, ${name}!`;
};
// console.log(greet("Alice")); // Error: Cannot access before initialization
- Hoisting: Змінна піднімається як
undefined, ініціалізація — ні - Використання: Динамічне створення функцій, присвоєння у змінні
Arrow Function (різновид expression):
const greet = (name: string): string => `Hello, ${name}!`;
- Не має власного
this - Компактніший синтаксис
void — функція завершується, але не повертає значення (або повертає undefined):
function log(message: string): void {
console.log(message);
// функція дійшла до кінця
}
never — функція ніколи не завершується нормально:
function throwError(message: string): never {
throw new Error(message);
// код після throw не виконується
}
function infiniteLoop(): never {
while (true) {
// цикл ніколи не завершиться
}
}
Різниця:
void: функція виконується → завершується → повертає керування (значенняundefined)never: функція або викидає помилку, або ніколи не закінчується
Rest parameters (...params) — збирає кілька аргументів у масив (у визначенні функції):
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // numbers = [1, 2, 3, 4]
Spread operator (...array) — розгортає масив у окремі аргументи (при виклику):
let nums = [1, 2, 3];
console.log(Math.max(...nums)); // Math.max(1, 2, 3)
Правило:
- Rest — у параметрах функції (збирає аргументи)
- Spread — у виклику функції (розгортає масив)
JavaScript/TypeScript передає аргументи позиційно (за порядком):
// ❌ Якби можна було так:
function example(optional?: string, required: number): void { }
example(42); // Що передається: optional=42 чи required=42?
Неоднозначність! TypeScript не знає, чи 42 — це пропущений optional і передається required, чи це optional.
Рішення: Опціональні параметри в кінці:
// ✅ Правильно
function example(required: number, optional?: string): void { }
example(42); // required=42, optional=undefined
example(42, "hello"); // required=42, optional="hello"
Тепер все однозначно!
Arrow functions були додані в ES6 саме для вирішення проблеми з this у callbacks:
Проблема зі звичайними функціями:
class Timer {
seconds = 0;
start() {
setInterval(function() {
this.seconds++; // Error: this тут undefined!
}, 1000);
}
}
this у callback втрачається, бо функція викликається в іншому контексті.
Рішення: arrow function успадковує this:
class Timer {
seconds = 0;
start() {
setInterval(() => {
this.seconds++; // OK: this = екземпляр Timer
}, 1000);
}
}
Arrow function не створює власний this, а успадковує його з місця визначення. Це робить callbacks безпечними.
Використовуйте overloading, коли:
- Тип повернення залежить від типу входу:
function process(input: string): string;
function process(input: number): number;
function process(input: string | number): string | number {
// реалізація
}
- Різні комбінації параметрів:
function createElement(tag: string): HTMLElement;
function createElement(tag: string, attrs: object): HTMLElement;
НЕ використовуйте overloading для:
- Опціональних параметрів (використовуйте
?) - Default значень (використовуйте
=) - Простих union types
Правило: Overloading — для складних випадків, де union type недостатньо точний.