$ sudo teach IT
МОДУЛЬ 4 · УРОК 5

Стрелочные функции и типизация

Arrow functions в TypeScript — компактный синтаксис, контекст this, типизация параметров и возвращаемого значения.

⏱ ~22 минут 🎓 Базовый уровень 📌 Функции

🎯 Что такое стрелочные функции?

Стрелочные функции (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 вопросов

Стрелочная функция-фильтр

Premium