25. Call Signatures
TypeScript: Броня. Урок 24: Call Signatures (Сигнатуры вызова)
Заголовок раздела «TypeScript: Броня. Урок 24: Call Signatures (Сигнатуры вызова)»Call signatures определяют, как объект или интерфейс может быть вызван как функция. Это позволяет создавать callable objects - объекты, которые одновременно являются функциями и имеют дополнительные свойства. Call signatures критически важны для понимания типизации функций и создания сложных API.
Базовый синтаксис
Заголовок раздела «Базовый синтаксис»// Call signature в интерфейсеinterface Greeter { (name: string): string;}
// Реализацияconst greet: Greeter = (name) => { return `Hello, ${name}!`;};
console.log(greet('Alice')); // "Hello, Alice!"
// Альтернативный синтаксис с type aliastype Multiplier = (x: number, y: number) => number;
const multiply: Multiplier = (x, y) => x * y;console.log(multiply(3, 4)); // 12Call Signatures с дополнительными свойствами
Заголовок раздела «Call Signatures с дополнительными свойствами»// Callable object с свойствамиinterface Counter { // Call signature (): number;
// Свойства count: number; reset(): void;}
// Создание callable objectfunction createCounter(): Counter { // Функция const counter = (() => { counter.count++; return counter.count; }) as Counter;
// Свойства counter.count = 0; counter.reset = () => { counter.count = 0; };
return counter;}
// Использованиеconst counter = createCounter();console.log(counter()); // 1console.log(counter()); // 2console.log(counter()); // 3console.log(counter.count); // 3counter.reset();console.log(counter()); // 1Перегрузки в Call Signatures
Заголовок раздела «Перегрузки в Call Signatures»// Множественные call signatures (перегрузки)interface Formatter { (value: number): string; (value: Date): string; (value: boolean): string;}
const format: Formatter = (value: number | Date | boolean): string => { if (typeof value === 'number') { return value.toFixed(2); } if (value instanceof Date) { return value.toISOString(); } return value ? 'Yes' : 'No';};
// TypeScript знает все перегрузкиconsole.log(format(42)); // "42.00"console.log(format(new Date())); // "2024-01-01T00:00:00.000Z"console.log(format(true)); // "Yes"
// Более сложный пример: API clientinterface ApiClient { <T>(endpoint: string): Promise<T>; <T>(endpoint: string, options: { method: 'GET' }): Promise<T>; <T>(endpoint: string, options: { method: 'POST'; body: any }): Promise<T>;}
const api: ApiClient = async <T>( endpoint: string, options?: any): Promise<T> => { const response = await fetch(endpoint, options); return response.json();};
// Type-safe использованиеinterface User { id: string; name: string;}
const user = await api<User>('/api/user');const users = await api<User[]>('/api/users', { method: 'GET' });const created = await api<User>('/api/users', { method: 'POST', body: { name: 'Alice' },});Generic Call Signatures
Заголовок раздела «Generic Call Signatures»// Generic функция через call signatureinterface Transform { <T, U>(value: T, fn: (val: T) => U): U;}
const transform: Transform = (value, fn) => fn(value);
const result1 = transform(42, x => x.toString()); // stringconst result2 = transform('hello', x => x.length); // numberconst result3 = transform([1, 2, 3], x => x.join(',')); // string
// Generic с ограничениямиinterface Mapper { <T extends object, K extends keyof T>(obj: T, key: K): T[K];}
const getProperty: Mapper = (obj, key) => obj[key];
const user = { name: 'Alice', age: 30 };const name = getProperty(user, 'name'); // stringconst age = getProperty(user, 'age'); // number
// Множественные generic параметрыinterface Reducer { <T, U>(array: T[], fn: (acc: U, val: T) => U, initial: U): U;}
const reduce: Reducer = (array, fn, initial) => { let acc = initial; for (const val of array) { acc = fn(acc, val); } return acc;};
const sum = reduce([1, 2, 3, 4], (acc, val) => acc + val, 0); // number: 10const joined = reduce(['a', 'b', 'c'], (acc, val) => acc + val, ''); // string: "abc"Практический пример: Query Builder
Заголовок раздела «Практический пример: Query Builder»// Type-safe query builder с call signaturesinterface QueryBuilder<T> { // Call signature возвращает новый builder <K extends keyof T>(field: K, value: T[K]): QueryBuilder<T>;
// Методы where<K extends keyof T>(field: K, value: T[K]): QueryBuilder<T>; orderBy<K extends keyof T>(field: K, direction?: 'asc' | 'desc'): QueryBuilder<T>; limit(count: number): QueryBuilder<T>; execute(): Promise<T[]>;
// Свойства readonly query: string;}
function createQueryBuilder<T>(): QueryBuilder<T> { const conditions: string[] = []; let orderClause = ''; let limitClause = '';
const builder: any = function(field: keyof T, value: any) { return builder.where(field, value); };
builder.where = function(field: keyof T, value: any) { conditions.push(`${String(field)} = ${JSON.stringify(value)}`); return builder; };
builder.orderBy = function(field: keyof T, direction = 'asc') { orderClause = `ORDER BY ${String(field)} ${direction}`; return builder; };
builder.limit = function(count: number) { limitClause = `LIMIT ${count}`; return builder; };
builder.execute = async function() { const query = [ 'SELECT *', `WHERE ${conditions.join(' AND ')}`, orderClause, limitClause, ].filter(Boolean).join(' ');
// Выполнение запроса console.log(query); return [] as T[]; };
Object.defineProperty(builder, 'query', { get() { return [ 'SELECT *', conditions.length ? `WHERE ${conditions.join(' AND ')}` : '', orderClause, limitClause, ].filter(Boolean).join(' '); }, });
return builder as QueryBuilder<T>;}
// Использованиеinterface User { id: string; name: string; email: string; age: number;}
const query = createQueryBuilder<User>();
// Все варианты type-safequery('name', 'Alice').execute();query.where('age', 30).orderBy('name', 'desc').limit(10).execute();
console.log(query('name', 'Bob').query); // "SELECT * WHERE name = \"Bob\""Жизненный пример: Event Emitter
Заголовок раздела «Жизненный пример: Event Emitter»// Type-safe event emitter с call signaturesinterface EventMap { [event: string]: any;}
interface EventEmitter<T extends EventMap> { // Call signature для emit <K extends keyof T>(event: K, payload: T[K]): void;
// Методы on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void; off<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void; once<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void;
// Свойства listenerCount<K extends keyof T>(event: K): number;}
function createEventEmitter<T extends EventMap>(): EventEmitter<T> { const listeners: Map<keyof T, Set<Function>> = new Map();
const emitter: any = function(event: keyof T, payload: any) { const handlers = listeners.get(event); if (handlers) { handlers.forEach(handler => handler(payload)); } };
emitter.on = function(event: keyof T, handler: Function) { if (!listeners.has(event)) { listeners.set(event, new Set()); } listeners.get(event)!.add(handler); };
emitter.off = function(event: keyof T, handler: Function) { listeners.get(event)?.delete(handler); };
emitter.once = function(event: keyof T, handler: Function) { const wrapper = (payload: any) => { handler(payload); emitter.off(event, wrapper); }; emitter.on(event, wrapper); };
emitter.listenerCount = function(event: keyof T) { return listeners.get(event)?.size ?? 0; };
return emitter as EventEmitter<T>;}
// Определение событийinterface AppEvents { 'user:login': { userId: string; timestamp: number }; 'user:logout': { userId: string }; 'data:update': { key: string; value: any }; 'error': { message: string; code: number };}
// Использованиеconst emitter = createEventEmitter<AppEvents>();
// Type-safe listenersemitter.on('user:login', (data) => { console.log(`User ${data.userId} logged in at ${data.timestamp}`);});
emitter.on('error', (err) => { console.error(`Error ${err.code}: ${err.message}`);});
// Type-safe emit через call signatureemitter('user:login', { userId: '123', timestamp: Date.now(),});
emitter('error', { message: 'Something went wrong', code: 500,});
// Или через метод emit (если добавить)// emitter.emit('user:logout', { userId: '123' });Construct Signatures
Заголовок раздела «Construct Signatures»// Construct signature (для конструкторов)interface Constructable<T> { new (...args: any[]): T;}
// Generic фабрикаfunction createInstance<T>(constructor: Constructable<T>, ...args: any[]): T { return new constructor(...args);}
class User { constructor(public name: string, public age: number) {}}
class Product { constructor(public title: string, public price: number) {}}
const user = createInstance(User, 'Alice', 30);const product = createInstance(Product, 'Laptop', 999);
// Комбинирование call и construct signaturesinterface Builder<T> { // Call signature (): T;
// Construct signature new (): T;
// Статические методы create(): T;}
class UserBuilder implements Builder<User> { // Call signature implementation (через static) static create(): User { return new User('Default', 0); }
// Construct signature implementation constructor() { return new User('Built', 25); }}
// Оба способа работаютconst user1 = UserBuilder.create(); // через callconst user2 = new UserBuilder(); // через constructCall Signatures в Type Aliases
Заголовок раздела «Call Signatures в Type Aliases»// Type alias с call signaturetype Logger = { (message: string): void; level: 'info' | 'warn' | 'error'; setLevel(level: 'info' | 'warn' | 'error'): void;};
const logger: Logger = Object.assign( (message: string) => { console.log(`[${logger.level}] ${message}`); }, { level: 'info' as const, setLevel(newLevel: 'info' | 'warn' | 'error') { logger.level = newLevel; }, });
// Использованиеlogger('Application started'); // [info] Application startedlogger.setLevel('warn');logger('Warning message'); // [warn] Warning messageAsync Call Signatures
Заголовок раздела «Async Call Signatures»// Асинхронные call signaturesinterface AsyncFetcher { <T>(url: string): Promise<T>; <T>(url: string, options: RequestInit): Promise<T>;
cache: Map<string, any>; clearCache(): void;}
const fetcher: AsyncFetcher = Object.assign( async <T>(url: string, options?: RequestInit): Promise<T> => { const cacheKey = `${url}:${JSON.stringify(options)}`;
if (fetcher.cache.has(cacheKey)) { return fetcher.cache.get(cacheKey); }
const response = await fetch(url, options); const data = await response.json();
fetcher.cache.set(cacheKey, data); return data; }, { cache: new Map<string, any>(), clearCache() { this.cache.clear(); }, });
// Type-safe использованиеinterface Post { id: number; title: string; body: string;}
const post = await fetcher<Post>('/api/posts/1');const posts = await fetcher<Post[]>('/api/posts', { method: 'GET' });
console.log(fetcher.cache.size); // 2fetcher.clearCache();Ключевые моменты
Заголовок раздела «Ключевые моменты»- Call signatures определяют, как интерфейс/тип может быть вызван как функция
- Синтаксис:
(param: Type): ReturnTypeвнутри интерфейса/типа - Позволяют создавать callable objects с дополнительными свойствами
- Поддерживают перегрузки функций
- Generic call signatures обеспечивают type safety для параметризованных функций
- Construct signatures для конструкторов:
new (...args: any[]): T - Можно комбинировать call и construct signatures в одном типе
- Используются для создания fluent API, builders, event emitters
- Критически важны для понимания типизации функций в TypeScript
- Альтернатива: type aliases с arrow function syntax