Стрелочные функции и типизация
Arrow functions в TypeScript — компактный синтаксис, контекст this, типизация параметров и возвращаемого значения.
🎯 Что такое стрелочные функции?
Стрелочные функции (arrow functions) — это компактный способ записи функций, появившийся в ES6. В TypeScript они получают дополнительные возможности благодаря типизации.
// Обычная функция
function add(a: number, b: number): number {
return a + b;
}
// Стрелочная функция — то же самое, но компактнее
const add = (a: number, b: number): number => {
return a + b;
};
// Ещё компактнее — неявный возврат
const add = (a: number, b: number): number => a + b;
Стрелочные функции особенно удобны для callback-функций, которые передаются как аргументы в другие функции:
// Без стрелочной функции
[1, 2, 3].map(function(n) { return n * 2; });
// Со стрелочной функцией
[1, 2, 3].map((n) => n * 2);
// Ещё компактнее для одного параметра
[1, 2, 3].map(n => n * 2);
💡 Аналогия: Стрелочная функция — как записка: «н = а + б». Короткая, но без лишней информации. Обычная функция — как письмо с шапкой и подписью: более подробное, но более громоздкое.
📝 Синтаксис и типизация стрелочных функций
В TypeScript стрелочные функции типизируются так же, как обычные. Рассмотрим все варианты синтаксиса.
Базовые варианты
// 1. Полный синтаксис — с телом и явным return
const greet = (name: string): string => {
return "Привет, " + name + "!";
};
// 2. Неявный возврат — для однострочных выражений
const greet = (name: string): string => "Привет, " + name + "!";
// 3. Один параметр — скобки необязательны
const square = (n: number): number => n * n;
const square = (n: number) => n * n; // Вывод типа: number
// 4. Нет параметров — обязательны пустые скобки
const getTimestamp = (): number => Date.now();
// 5. Несколько параметров — скобки обязательны
const add = (a: number, b: number): number => a + b;
// 6. Rest-параметры
const sum = (...numbers: number[]): number =>
numbers.reduce((acc, n) => acc + n, 0);
Типизация возвращаемого значения
// Неявный возврат — TypeScript выводит тип автоматически
const double = (n: number) => n * 2; // Тип: number
const upper = (s: string) => s.toUpperCase(); // Тип: string
const isPositive = (n: number) => n > 0; // Тип: boolean
// Явный возврат — когда нужно уточнить тип
const parseAge = (input: string): number | null => {
const num = parseInt(input, 10);
if (isNaN(num)) return null;
return num;
};
// Возврат объекта — нужны скобки вокруг выражения
const createPoint = (x: number, y: number) => ({ x, y });
// ✅ return ({ x, y }) — скобки говорят «это выражение, а не блок кода»
// ❌ Без скобок — это будет интерпретировано как блок кода
const broken = (x: number, y: number) => { x, y };
// broken возвращает undefined!
⚠️ Ловушка с объектами:
// ❌ Неправильно — TypeScript видит блок кода, а не объект
const createUser = (name: string) => { name, age: 25 };
// Ошибка: ',' expected
// ✅ Правильно — оборачиваем в скобки
const createUser = (name: string) => ({ name, age: 25 });
// OK: возвращает объект { name: string; age: number }
🏷️ Тип функции в TypeScript
В TypeScript функции — это значения, которые можно типизировать. Есть несколько способов описать тип функции.
Способ 1: Callable syntax
// Тип функции: (параметры) => возвращаемый_ тип
type MathOperation = (a: number, b: number) => number;
// Использование типа
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
const multiply: MathOperation = (a, b) => a * b;
// Функция, принимающая callback определённого типа
function calculate(a: number, b: number, operation: MathOperation): number {
return operation(a, b);
}
calculate(5, 3, add); // 8
calculate(5, 3, subtract); // 2
calculate(5, 3, multiply); // 15
Способ 2: Interface с сигнатурой вызова
// Интерфейс для функции
interface SearchFunction {
(query: string, options?: { caseSensitive?: boolean }): string[];
}
// Реализация
const search: SearchFunction = (query, options = {}) => {
const items = ["Hello", "World", "TypeScript", "JavaScript"];
const { caseSensitive = false } = options;
return items.filter(item => {
const searchIn = caseSensitive ? item : item.toLowerCase();
const searchQuery = caseSensitive ? query : query.toLowerCase();
return searchIn.includes(searchQuery);
});
};
search("script"); // ["TypeScript", "JavaScript"]
search("Script", { caseSensitive: false }); // ["TypeScript", "JavaScript"]
search("Script", { caseSensitive: true }); // []
Способ 3: Тип функции для стрелочных
// Типы для различных callback-функций
type Callback = () => void;
type ErrorHandler = (error: Error) => void;
type DataProcessor = (data: T) => T;
type Predicate = (item: T) => boolean;
type Mapper = (item: T) => R;
type Reducer = (acc: R, item: T) => R;
// Использование
function filter(array: T[], predicate: Predicate): T[] {
return array.filter(predicate);
}
function map(array: T[], mapper: Mapper): R[] {
return array.map(mapper);
}
function reduce(array: T[], reducer: Reducer, initial: R): R {
return array.reduce(reducer, initial);
}
// Вызовы с типизированными callbacks
const numbers = [1, 2, 3, 4, 5];
const evens = filter(numbers, n => n % 2 === 0); // [2, 4]
const doubled = map(numbers, n => n * 2); // [2, 4, 6, 8, 10]
const sum = reduce(numbers, (acc, n) => acc + n, 0); // 15
🔖 Контекст this в стрелочных функциях
Одно из главных отличий стрелочных функций от обычных — они не имеют своего контекста this. Вместо этого они используют this из окружающего (лексического) контекста.
class Timer {
seconds = 0;
// ❌ Обычная функция — this зависит от того, КАК вызвана
startBad(): void {
setInterval(function() {
this.seconds++; // this = window (в браузере) или undefined
console.log(this.seconds); // Ошибка! this не указывает на Timer
}, 1000);
}
// ✅ Стрелочная функция — this = this класса (лексический контекст)
startGood(): void {
setInterval(() => {
this.seconds++; // this = экземпляр Timer
console.log(this.seconds); // Работает!
}, 1000);
}
}
this в колбэках
class UserList {
private users: string[] = ["Анна", "Борис", "Вика"];
// Стрелочная функция сохраняет контекст this
logUsers(): void {
this.users.forEach((user, index) => {
// this здесь — экземпляр UserList (благодаря стрелочной функции)
console.log(`${index + 1}. ${user} из ${this.constructor.name}`);
});
}
// Без стрелочной функции this потеряется
logUsersBad(): void {
this.users.forEach(function(user, index) {
console.log(`${index + 1}. ${user}`);
console.log(this); // undefined! (в строгом режиме)
});
}
}
Стрелочные функции и объектный тип this
// TypeScript позволяет типизировать this в функциях
interface Formatter {
this: { prefix: string; suffix: string };
(value: string): string;
}
// Создаём функцию и типизируем this
function createFormatter(this: { prefix: string; suffix: string }, value: string): string {
return this.prefix + value + this.suffix;
}
// Привязываем контекст
const formatter = createFormatter.bind({ prefix: "[", suffix: "]" });
formatter("hello"); // "[hello]"
// Или через объект
const obj = {
prefix: ">>> ",
suffix: " <<<",
format: createFormatter
};
obj.format("test"); // ">>> test <<<"
✅ Правило: Используйте стрелочные функции в коллбэках, где нужен доступ к this из внешнего контекста. Обычные функции используйте как методы объектов и классов.
📐 Стрелочные функции как типы
Стрелочные функции используются повсеместно в TypeScript — как типы для параметров, переменных иgenerics ограничений.
Типы функций для параметров
// Функция принимает callback
function fetchData(url: string, callback: (data: unknown) => void): void {
fetch(url)
.then(res => res.json())
.then(data => callback(data));
}
// С дженериком — более типобезопасно
function fetchData(url: string, callback: (data: T) => void): void {
fetch(url)
.then(res => res.json())
.then(data => callback(data as T));
}
// Вызов
fetchData("/api/users", (users) => {
// users имеет тип User[]
users.forEach(user => console.log(user.name));
});
Типы функций для переменных
// Тип переменной — стрелочная функция
type Validator = (input: string) => boolean;
const isEmail: Validator = (input) => input.includes("@");
const isPhone: Validator = (input) => /^\d{10,}$/.test(input);
const isEmpty: Validator = (input) => input.trim().length === 0;
// Использование
function validate(input: string, validators: Validator[]): string[] {
const errors: string[] = [];
if (!isEmail(input)) errors.push("Неверный email");
if (!isPhone(input)) errors.push("Неверный телефон");
if (isEmpty(input)) errors.push("Пустое значение");
return errors;
}
Типы функций дляgenerics ограничений
// Ограничиваемgenerics типом функции
function memoize unknown>(
fn: T
): T {
const cache = new Map();
return ((...args: unknown[]) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
}) as T;
}
// Использование
const expensiveCalculation = memoize((n: number): number => {
console.log("Вычисление для", n);
return n * n;
});
expensiveCalculation(5); // Вычисление для 5 → 25
expensiveCalculation(5); // Кэш → 25 (без вычисления)
⚖️ Стрелочные vs обычные функции: сравнение
| Характеристика | Обычная функция | Стрелочная функция |
|---|---|---|
| Синтаксис | function name() {} |
const name = () => {} |
| this | Динамический (от вызова) | Лексический (из контекста) |
| arguments | Есть | Нет (используйте rest) |
| Хостинг (hoisting) | Есть (function declarations) | Нет |
| Метод объекта | ✅ Подходит | ⚠️ Потеря this |
| Callback | ⚠️ Проблемы с this | ✅ Идеально |
// Пример: почему this важен
class Counter {
count = 0;
// ❌ Обычная функция — this не сохраняется
incrementBad(): void {
setInterval(function() {
this.count++; // this = window, а не Counter
}, 1000);
}
// ✅ Стрелочная функция — this сохраняется
incrementGood(): void {
setInterval(() => {
this.count++; // this = Counter
}, 1000);
}
}
// Метод объекта — лучше обычная функция
const calculator = {
value: 0,
// ✅ Обычная функция — this = calculator
add(n: number): void {
this.value += n;
},
// ⚠️ Стрелочная функция — this = внешний контекст (не calculator)
addBad: (n: number): void => {
// this.value не работает — this не calculator!
}
};
🛠️ Практические паттерны
Паттерн 1: Композиция функций
// Композиция: объединение функций в цепочку
function compose(
f: (a: A) => B,
g: (b: B) => C
): (a: A) => C {
return (a) => g(f(a));
}
// Использование
const processString = compose(
(s: string) => s.trim(),
(s: string) => s.toUpperCase()
);
processString(" hello "); // "HELLO"
// Тройная композиция
const processNumber = compose(
(n: number) => n * 2,
(n: number) => n + 10,
(n: number) => n.toString()
);
processNumber(5); // "20"
Паттерн 2: Pipe (последовательное применение)
// Pipe: применяет функции слева направо
function pipe(value: T, ...fns: Array<(arg: T) => T>): T {
return fns.reduce((acc, fn) => fn(acc), value);
}
// Использование
const result = pipe(
"Hello, World!",
(s) => s.trim(),
(s) => s.toLowerCase(),
(s) => s.replace(/,/g, ""),
(s) => s.split(" ").join("_")
);
// "hello_world"
Паттерн 3: Debounce и Throttle
// Debounce: откладывает вызов до паузы
function debounce unknown>(
fn: T,
delay: number
): (...args: Parameters) => void {
let timeoutId: ReturnType;
return (...args: Parameters) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
// Throttle: ограничивает частоту вызовов
function throttle unknown>(
fn: T,
limit: number
): (...args: Parameters) => void {
let inThrottle = false;
return (...args: Parameters) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// Использование
const handleSearch = debounce((query: string) => {
console.log("Поиск:", query);
}, 300);
const handleScroll = throttle(() => {
console.log("Скролл!");
}, 100);
Паттерн 4: Curry (частичное применение)
// Curry: превращает функцию с несколькими аргументами в цепочку
function curry(
fn: (...args: A) => R
): (...args: A) => R {
return fn; // В TypeScript curry проще — просто возвращаем функцию
}
// Более практичный пример: фиксируем первые аргументы
function partial(
fn: (...args: A) => R,
...fixedArgs: Partial
): (...restArgs: A) => R {
return (...restArgs) => fn(...fixedArgs, ...restArgs) as R;
}
// Использование
function createUrl(base: string, path: string, params: Record): string {
const query = Object.entries(params).map(([k, v]) => `${k}=${v}`).join("&");
return `${base}${path}?${query}`;
}
const apiUrl = partial(createUrl, "https://api.example.com");
apiUrl("/users", { format: "json" }); // "https://api.example.com/users?format=json"
🔧 Стрелочные функции иgenerics (Generics)
Стрелочные функции отлично работают сgenerics, создавая типобезопасные утилиты.
// Фильтрация сgenerics
function filter(array: T[], predicate: (item: T) => boolean): T[] {
return array.filter(predicate);
}
filter([1, 2, 3, 4, 5], n => n > 3); // [4, 5]
filter(["a", "b", "c"], s => s !== "b"); // ["a", "c"]
// Маппинг сgenerics
function map(array: T[], transform: (item: T) => R): R[] {
return array.map(transform);
}
map([1, 2, 3], n => n * 2); // [2, 4, 6]
map(["a", "b"], s => s.length); // [1, 1]
// Редуция сgenerics
function reduce(
array: T[],
reducer: (acc: R, item: T) => R,
initial: R
): R {
return array.reduce(reducer, initial);
}
reduce([1, 2, 3], (acc, n) => acc + n, 0); // 6
reduce([1, 2, 3], (acc, n) => acc + n, ""); // "123"
// Цепочка: filter → map → reduce
const result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.filter(n => n % 2 === 0) // [2, 4, 6, 8, 10]
.map(n => n * n); // [4, 16, 36, 64, 100]
// result: number[]
❌ Частые ошибки
Ошибка 1: Возврат объекта без скобок
// ❌ Ошибка — это блок кода, не объект
const createUser = (name: string) => { name: string; age: number };
// Ошибка: ',' expected
// ✅ Правильно — скобки вокруг выражения
const createUser = (name: string) => ({ name, age: 25 });
Ошибка 2: Потеря this
class Timer {
count = 0;
// ❌ this потеряется
start(): void {
setInterval(function() {
this.count++; // this = window, не Timer
}, 1000);
}
// ✅ Используем стрелочную функцию
startFixed(): void {
setInterval(() => {
this.count++; // this = Timer
}, 1000);
}
}
Ошибка 3: Забыли типизировать callback
// ❌ Плохо — callback не типизирован
[1, 2, 3].map(n => n * 2); // Работает, но лучше явно
// ✅ Хорошо — явная типизация
[1, 2, 3].map((n: number): number => n * 2);
// ✅ Или через переменную
const double = (n: number): number => n * 2;
[1, 2, 3].map(double);
Ошибка 4: Стрелочная функция как метод объекта
// ❌ Стрелочная функция как метод — this не работает
const obj = {
value: 42,
getValue: () => {
return this.value; // this ≠ obj!
}
};
// ✅ Обычная функция как метод
const obj = {
value: 42,
getValue(): number {
return this.value; // this = obj
}
};
📋 Итоги урока
- Синтаксис:
const fn = (param: Type) => expression - Неявный возврат: для однострочных выражений можно опустить
returnи{} - Объекты: оборачивайте в скобки
() => ({}) - Тип функции:
(param: T) => Rили черезtype/interface - this: стрелочные функции берут
thisиз лексического контекста - Используйте: в коллбэках, композиции, утилитах сgenerics
- Не используйте: как методы объектов, где нужен
this
✅ Ключевое правило: Стрелочные функции — компактный и типобезопасный способ создания функций в TypeScript. Используйте их везде, где нужна короткая функция и нет потерь контекста this.
В следующем уроке мы разберём типизацию callback-функций — как передавать функции как параметры и типизировать их.
Проверяем понимание
6 вопросов