Интерфейсы для функций
Callable interfaces — как типизировать функции через интерфейсы
Цели урока
- Понять, что такое callable interfaces
- Научиться описывать сигнатуры функций через интерфейсы
- Понять, когда использовать callable interfaces vs type aliases
- Освоить практические паттерны использования
- Изучить комбинацию вызываемости и дополнительных свойств
Что такое callable interface?
Callable interface — это интерфейс, который описывает объект, который можно вызвать как функцию. По сути, это способ описать тип функции через интерфейс, добавляя возможность определять дополнительные свойства и методы.
Обычные функции в JavaScript — это объекты. Они могут иметь свойства и методы. Callable interfaces позволяют типизировать такие «функции-объекты», описывая и сигнатуру вызова, и дополнительные свойства. Это особенно полезно для функций с метаданными, конфигурацией или вспомогательными методами.
В JavaScript функции являются объектами первого класса, что означает, что они могут иметь свойства, передаваться как аргументы, возвращаться из других функций и даже иметь собственные методы. Callable interfaces позволяют типизировать такие структуры.
Синтаксис callable interface
В интерфейсе сигнатура функции описывается без имени — просто параметры и возвращаемый тип:
interface Greeter {
(name: string): string;
}
// Создаём функцию, соответствующую интерфейсу
const greet: Greeter = (name) => {
return `Привет, ${name}!`;
};
console.log(greet("Алексей")); // "Привет, Алексей!"
// Интерфейс с несколькими параметрами
interface Calculator {
(a: number, b: number): number;
}
// Интерфейс без параметров
interface Initializer {
(): void;
}
Обратите внимание: в интерфейсе сигнатура вызова записывается без имени функции — только параметры в скобках и возвращаемый тип после двоеточия. Это называется «call signature» (сигнатура вызова).
Сигнатура вызова должна быть первым элементом интерфейса, если вы хотите, чтобы объект был вызываемым. Это соглашение TypeScript для идентификации callable интерфейсов.
Callable interface с дополнительными свойствами
Главная сила callable interfaces — возможность комбинировать вызываемость с дополнительными свойствами:
// Сигнатура вызова
(message: string): void;
// Дополнительные свойства
level: "info" | "warn" | "error";
count: number;
reset(): void;
}
// Создаём логгер
function createLogger(level: Logger["level"]): Logger {
let count = 0;
// Создаём функцию
const log = (message: string): void => {
count++;
console.log(`[${level}] ${message}`);
};
// Добавляем свойства
(log as Logger).level = level;
(log as Logger).count = count;
(log as Logger).reset = () => { count = 0; };
return log as Logger;
}
// Использование
const logger = createLogger("info");
logger("Запуск приложения"); // Вызываем как функцию
logger("Загрузка данных");
console.log(logger.count); // 2 — используем как объект
logger.reset(); // Вызываем метод
console.log(logger.count); // 0
Теперь logger — это и функция (можно вызвать logger("message")), и объект (можно прочитать logger.count или вызвать logger.reset()).
Практический пример: функция с метаданными
Рассмотрим пример функции-мемоизации, которая хранит результаты кэширования:
// Сигнатура вызова
(arg: T): R;
// Метаданные кэша
cache: Map<T, R>;
hits: number;
misses: number;
clearCache(): void;
}
// Функция мемоизации
function memoize<T, R>(
fn: (arg: T) => R
): MemoizedFunction<T, R> {
const cache = new Map<T, R>();
let hits = 0;
let misses = 0;
const memoized = (arg: T): R => {
if (cache.has(arg)) {
hits++;
return cache.get(arg)!;
}
misses++;
const result = fn(arg);
cache.set(arg, result);
return result;
};
(memoized as MemoizedFunction<T, R>).cache = cache;
(memoized as MemoizedFunction<T, R>).hits = () => hits;
(memoized as MemoizedFunction<T, R>).misses = () => misses;
(memoized as MemoizedFunction<T, R>).clearCache = () => cache.clear();
return memoized as MemoizedFunction<T, R>;
}
// Использование
const expensiveCalc = memoize((n: number) => {
console.log(`Вычисление для ${n}`);
return n * n;
});
expensiveCalc(5); // Вычисление для 5 → 25
expensiveCalc(5); // Из кэша → 25
expensiveCalc(3); // Вычисление для 3 → 9
console.log(Hits: ${expensiveCalc.hits}`); // 1
console.log(`Misses: ${expensiveCalc.misses}`);// 2
Callable interface vs Function type
TypeScript позволяет описывать типы функций двумя способами. Вот их сравнение:
type GreetFn = (name: string) => string;
// Callable interface — расширенный способ
interface GreetInterface {
(name: string): string;
description: string;
version: number;
deprecated: boolean;
}
- Простой синтаксис
- Только сигнатура вызова
- Лучше для простых случаев
- Более компактный
- Более мощный
- Поддерживает дополнительные свойства
- Лучше для «функций-объектов»
- Может содержать методы
Используйте function type для простых функций, а callable interface — когда функция имеет дополнительные свойства или методы. Callable interface также лучше для документирования намерений функции.
Паттерн: функция-валидатор
Рассмотрим паттерн создания функции-валидатора с метаданными:
(value: T): boolean;
name: string;
errorMessage: string;
}
// Фабрика валидаторов
function createValidator<T>(
name: string,
errorMessage: string,
validate: (value: T) => boolean
): Validator<T> {
const validator = validate as Validator<T>;
validator.name = name;
validator.errorMessage = errorMessage;
return validator;
}
// Создаём валидаторы
const isEmail = createValidator<string>(
"email",
"Некорректный email",
(v) => v.includes("@") && v.includes(".")
);
const isPositive = createValidator<number>(
"positive",
"Значение должно быть положительным",
(v) => v > 0
);
// Использование
console.log(isEmail("test@example.com")); // true
console.log(isEmail.name); // "email"
console.log(isEmail.errorMessage); // "Некорректный email"
// Валидация формы
function validateField<T>(value: T, validator: Validator<T>): string | null {
if (validator(value)) {
return null;
}
return validator.errorMessage;
}
Паттерн: цепочка вызовов
Callable interface позволяет реализовать паттерн цепочки вызовов (fluent interface):
(): T[];
where(predicate: (item: T) => boolean): QueryBuilder<T>;
select<K extends keyof T>(key: K): QueryBuilder<T[K]>;
limit(n: number): QueryBuilder<T>;
}
// Пример использования:
// const results = query
// .where(item => item.active)
// .select("name")
// .limit(10);
Такой интерфейс позволяет создавать цепочки вызовов, при этом сам интерфейс вызываемый. Это делает код более декларативным и читаемым.
Распространённые ошибки
level: string;
count: number;
}
// Logger не вызываемый!
(msg: string): void;
level: string;
count: number;
}
// Logger вызываемый!
const f: Fn = () => {};
// Ошибка: нет свойства x!
const f: Fn = Object.assign(
() => {}, { x: 1 }
);
// OK!
Паттерн: функция с состоянием
Callable interface позволяет создавать функции с внутренним состоянием:
(): number;
value: number;
reset(): void;
increment(n?: number): void;
}
// Создаём счётчик
function createCounter(initial = 0): Counter {
let count = initial;
const counter = (): number => count++;
Object.defineProperties(counter, {
value: { get: () => count },
reset: { value: () => { count = initial; } },
increment: { value: (n = 1) => { count += n; } }
});
return counter as Counter;
}
// Использование
const counter = createCounter(10);
console.log(counter()); // 10
console.log(counter()); // 11
console.log(counter.value); // 12
counter.increment(5);
console.log(counter.value); // 17
counter.reset();
console.log(counter.value); // 10
Такой паттерн позволяет создавать функции, которые можно вызывать как простые функции, но при этом имеют дополнительные методы для управления состоянием.
Паттерн: событийный эмиттер
Callable interface идеально подходит для реализации событийных эмиттеров:
type EventHandler = (data: any) => void;
// Callable interface для эмиттера
interface EventEmitter {
(event: string, data?: any): void;
on(event: string, handler: EventHandler): void;
off(event: string, handler: EventHandler): void;
once(event: string, handler: EventHandler): void;
}
// Использование
// emit("click", { x: 10, y: 20 });
// emit.on("click", handler);
// emit.once("load", handler);
Такой интерфейс позволяет вызывать эмиттер как функцию для отправки событий и использовать методы для управления подписками.
Практическое задание
Создайте callable interface DebouncedFunction для функции debounce:
// - Сигнатуру вызова: (args: any[]) => void
// - Свойство cancel: () => void
// - Свойство flush: () => void
// - Свойство pending: boolean
// Реализуйте функцию createDebouncedFunction
Чек-лист урока
- Callable interface — интерфейс с сигнатурой вызова
- Сигнатура записывается без имени:
(param: Type): ReturnType - Можно добавлять дополнительные свойства и методы
- Callable interface vs function type: interface для функций-объектов
- Паттерны: логирование, мемоизация, валидация, цепочки вызовов
- Используйте когда функция имеет метаданные или дополнительные методы
- Function type проще для простых случаев без свойств
Тест по callable интерфейсам
8 вопросов