+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 47 of 354

πŸ”¨ Callable and Constructable Interfaces: Function Types

Master callable and constructable interfaces in TypeScript to create powerful function and constructor type definitions πŸš€

πŸ’ŽAdvanced
25 min read

Prerequisites

  • Understanding of interfaces πŸ“
  • Function types knowledge πŸ”
  • Class and constructor basics πŸ’»

What you'll learn

  • Create callable interfaces for function types 🎯
  • Design constructable interfaces for classes πŸ—οΈ
  • Combine callable and regular properties πŸ›‘οΈ
  • Build type-safe factory patterns ✨

🎯 Introduction

Welcome to the powerful world of callable and constructable interfaces! πŸŽ‰ In this guide, we’ll explore how TypeScript allows you to define interfaces that describe functions and constructors, unlocking advanced type patterns.

You’ll discover how callable interfaces are like blueprints for functions πŸ“ - they define not just what a function looks like, but how it behaves. Whether you’re creating factory functions 🏭, designing plugin systems πŸ”Œ, or building framework APIs 🌐, understanding callable and constructable interfaces is essential for advanced TypeScript development.

By the end of this tutorial, you’ll be confidently creating interfaces that can be called, constructed, and still have properties! Let’s build some powerful types! πŸŠβ€β™‚οΈ

πŸ“š Understanding Callable and Constructable Interfaces

πŸ€” What are Callable Interfaces?

Callable interfaces define the signature of a function while also allowing you to add properties to that function. It’s like having a function that’s also an object - which is exactly what JavaScript functions are!

Think of callable interfaces like:

  • 🏭 Factory functions with configuration
  • πŸ”Œ Plugins with both functionality and metadata
  • 🎯 Event emitters that can be called directly
  • 🧰 Utility functions with attached helpers

πŸ’‘ What are Constructable Interfaces?

Constructable interfaces define constructor signatures - they describe how to create instances of a class. They’re perfect for defining factory patterns and ensuring constructor compatibility.

Real-world example: jQuery πŸ’° - it’s both a function $() and an object with properties like $.ajax. This dual nature is perfectly captured by callable interfaces!

πŸ”§ Callable Interfaces

πŸ“ Basic Callable Interface Syntax

Let’s start with the fundamentals:

// 🎯 Simple callable interface
interface SimpleGreeter {
  (name: string): string;
}

const greet: SimpleGreeter = (name) => {
  return `Hello, ${name}!`;
};

console.log(greet('Alice')); // "Hello, Alice!"

// πŸ”§ Callable interface with properties
interface AdvancedGreeter {
  // Call signature
  (name: string): string;
  
  // Properties
  language: string;
  formal: boolean;
  
  // Methods
  setLanguage(lang: string): void;
  reset(): void;
}

const createGreeter = (): AdvancedGreeter => {
  let language = 'en';
  let formal = false;
  
  const greeter = ((name: string) => {
    if (language === 'es') {
      return formal ? `Buenos dΓ­as, SeΓ±or/SeΓ±ora ${name}` : `Β‘Hola, ${name}!`;
    }
    return formal ? `Good day, ${name}` : `Hello, ${name}!`;
  }) as AdvancedGreeter;
  
  greeter.language = language;
  greeter.formal = formal;
  
  greeter.setLanguage = (lang: string) => {
    language = lang;
    greeter.language = lang;
  };
  
  greeter.reset = () => {
    language = 'en';
    formal = false;
    greeter.language = language;
    greeter.formal = formal;
  };
  
  return greeter;
};

const myGreeter = createGreeter();
console.log(myGreeter('John')); // "Hello, John!"

myGreeter.formal = true;
console.log(myGreeter('John')); // "Good day, John"

myGreeter.setLanguage('es');
console.log(myGreeter('Juan')); // "Buenos dΓ­as, SeΓ±or/SeΓ±ora Juan"

// πŸ“Š Multiple call signatures (overloading)
interface Calculator {
  // Overloaded signatures
  (a: number, b: number): number;
  (a: string, b: string): string;
  (a: number[], operation: 'sum' | 'avg' | 'max' | 'min'): number;
  
  // Properties
  precision: number;
  history: Array<{ operation: string; result: any }>;
  
  // Methods
  clearHistory(): void;
}

const createCalculator = (): Calculator => {
  const calc = ((a: any, b: any) => {
    // Handle different overloads
    if (typeof a === 'number' && typeof b === 'number') {
      const result = Number((a + b).toFixed(calc.precision));
      calc.history.push({ operation: `${a} + ${b}`, result });
      return result;
    }
    
    if (typeof a === 'string' && typeof b === 'string') {
      const result = a + b;
      calc.history.push({ operation: `"${a}" + "${b}"`, result });
      return result;
    }
    
    if (Array.isArray(a)) {
      let result: number;
      switch (b) {
        case 'sum':
          result = a.reduce((sum, n) => sum + n, 0);
          break;
        case 'avg':
          result = a.reduce((sum, n) => sum + n, 0) / a.length;
          break;
        case 'max':
          result = Math.max(...a);
          break;
        case 'min':
          result = Math.min(...a);
          break;
        default:
          result = 0;
      }
      calc.history.push({ operation: `${b}([${a.join(', ')}])`, result });
      return Number(result.toFixed(calc.precision));
    }
    
    return 0;
  }) as Calculator;
  
  calc.precision = 2;
  calc.history = [];
  
  calc.clearHistory = () => {
    calc.history = [];
  };
  
  return calc;
};

const calc = createCalculator();
console.log(calc(10, 20)); // 30
console.log(calc('Hello', ' World')); // "Hello World"
console.log(calc([1, 2, 3, 4, 5], 'avg')); // 3
console.log('History:', calc.history);

πŸ—οΈ Advanced Callable Patterns

Building more complex callable interfaces:

// πŸ”Œ Plugin system with callable interface
interface Plugin<T = any> {
  // Call signature for plugin execution
  (context: T): void | Promise<void>;
  
  // Plugin metadata
  name: string;
  version: string;
  description?: string;
  dependencies?: string[];
  
  // Lifecycle hooks
  install?(options?: any): void | Promise<void>;
  uninstall?(): void | Promise<void>;
  configure?(config: any): void;
  
  // Plugin capabilities
  supports?(feature: string): boolean;
}

class PluginManager<T> {
  private plugins: Map<string, Plugin<T>> = new Map();
  private installed: Set<string> = new Set();
  
  register(plugin: Plugin<T>): void {
    if (this.plugins.has(plugin.name)) {
      throw new Error(`Plugin '${plugin.name}' already registered`);
    }
    
    this.plugins.set(plugin.name, plugin);
    console.log(`πŸ“¦ Registered plugin: ${plugin.name} v${plugin.version}`);
  }
  
  async install(pluginName: string, options?: any): Promise<void> {
    const plugin = this.plugins.get(pluginName);
    if (!plugin) {
      throw new Error(`Plugin '${pluginName}' not found`);
    }
    
    if (this.installed.has(pluginName)) {
      console.log(`⚠️ Plugin '${pluginName}' already installed`);
      return;
    }
    
    // Check dependencies
    if (plugin.dependencies) {
      for (const dep of plugin.dependencies) {
        if (!this.installed.has(dep)) {
          await this.install(dep);
        }
      }
    }
    
    // Install plugin
    if (plugin.install) {
      await plugin.install(options);
    }
    
    this.installed.add(pluginName);
    console.log(`βœ… Installed plugin: ${pluginName}`);
  }
  
  async execute(context: T): Promise<void> {
    for (const pluginName of this.installed) {
      const plugin = this.plugins.get(pluginName)!;
      console.log(`πŸ”§ Executing plugin: ${plugin.name}`);
      await plugin(context);
    }
  }
}

// Create plugins
const loggingPlugin: Plugin<{ message: string }> = Object.assign(
  (context: { message: string }) => {
    console.log(`πŸ“ [Logger]: ${context.message}`);
  },
  {
    name: 'logger',
    version: '1.0.0',
    description: 'Simple logging plugin',
    
    install() {
      console.log('πŸ”§ Logger plugin installed');
    },
    
    supports(feature: string) {
      return ['console', 'file'].includes(feature);
    }
  }
);

const validationPlugin: Plugin<{ message: string }> = Object.assign(
  (context: { message: string }) => {
    if (!context.message || context.message.trim().length === 0) {
      throw new Error('Message cannot be empty');
    }
    console.log('βœ… Message validated');
  },
  {
    name: 'validator',
    version: '1.0.0',
    dependencies: ['logger']
  }
);

// 🎨 Event emitter with callable interface
interface EventEmitter<T = any> {
  // Emit event (callable)
  (event: string, data?: T): void;
  
  // Event methods
  on(event: string, handler: (data: T) => void): void;
  off(event: string, handler: (data: T) => void): void;
  once(event: string, handler: (data: T) => void): void;
  
  // Properties
  maxListeners: number;
  eventNames(): string[];
  listenerCount(event: string): number;
}

function createEventEmitter<T = any>(): EventEmitter<T> {
  const listeners = new Map<string, Set<(data: T) => void>>();
  
  const emitter = ((event: string, data?: T) => {
    const handlers = listeners.get(event);
    if (handlers) {
      handlers.forEach(handler => {
        try {
          handler(data!);
        } catch (error) {
          console.error(`Error in event handler for '${event}':`, error);
        }
      });
    }
  }) as EventEmitter<T>;
  
  emitter.maxListeners = 10;
  
  emitter.on = (event: string, handler: (data: T) => void) => {
    if (!listeners.has(event)) {
      listeners.set(event, new Set());
    }
    
    const handlers = listeners.get(event)!;
    if (handlers.size >= emitter.maxListeners) {
      console.warn(`⚠️ MaxListeners (${emitter.maxListeners}) exceeded for event '${event}'`);
    }
    
    handlers.add(handler);
  };
  
  emitter.off = (event: string, handler: (data: T) => void) => {
    listeners.get(event)?.delete(handler);
  };
  
  emitter.once = (event: string, handler: (data: T) => void) => {
    const onceHandler = (data: T) => {
      handler(data);
      emitter.off(event, onceHandler);
    };
    emitter.on(event, onceHandler);
  };
  
  emitter.eventNames = () => Array.from(listeners.keys());
  
  emitter.listenerCount = (event: string) => {
    return listeners.get(event)?.size || 0;
  };
  
  return emitter;
}

// πŸ”— Chainable API with callable interface
interface ChainableQuery<T> {
  // Execute query (callable)
  (): T[];
  
  // Chainable methods
  where(predicate: (item: T) => boolean): ChainableQuery<T>;
  orderBy<K extends keyof T>(key: K): ChainableQuery<T>;
  limit(count: number): ChainableQuery<T>;
  select<U>(selector: (item: T) => U): ChainableQuery<U>;
  
  // Properties
  count(): number;
  first(): T | undefined;
  last(): T | undefined;
}

function createQuery<T>(data: T[]): ChainableQuery<T> {
  let items = [...data];
  
  const query = (() => items) as ChainableQuery<T>;
  
  query.where = (predicate: (item: T) => boolean) => {
    return createQuery(items.filter(predicate));
  };
  
  query.orderBy = <K extends keyof T>(key: K) => {
    return createQuery([...items].sort((a, b) => {
      if (a[key] < b[key]) return -1;
      if (a[key] > b[key]) return 1;
      return 0;
    }));
  };
  
  query.limit = (count: number) => {
    return createQuery(items.slice(0, count));
  };
  
  query.select = <U>(selector: (item: T) => U) => {
    return createQuery(items.map(selector));
  };
  
  query.count = () => items.length;
  query.first = () => items[0];
  query.last = () => items[items.length - 1];
  
  return query;
}

// Usage example
interface User {
  id: number;
  name: string;
  age: number;
  active: boolean;
}

const users: User[] = [
  { id: 1, name: 'Alice', age: 30, active: true },
  { id: 2, name: 'Bob', age: 25, active: false },
  { id: 3, name: 'Charlie', age: 35, active: true }
];

const activeUsers = createQuery(users)
  .where(u => u.active)
  .orderBy('age')
  .limit(2)();

console.log('Active users:', activeUsers);

πŸ—οΈ Constructable Interfaces

πŸ“ Basic Constructor Signatures

Creating interfaces for constructors:

// 🏭 Simple constructor interface
interface Constructable<T> {
  new (...args: any[]): T;
}

interface PointConstructor {
  new (x: number, y: number): Point;
}

interface Point {
  x: number;
  y: number;
  distanceTo(other: Point): number;
}

class Point2D implements Point {
  constructor(public x: number, public y: number) {}
  
  distanceTo(other: Point): number {
    const dx = this.x - other.x;
    const dy = this.y - other.y;
    return Math.sqrt(dx * dx + dy * dy);
  }
}

// Type assertion to match constructor interface
const PointClass: PointConstructor = Point2D;
const point = new PointClass(10, 20);

// 🎨 Constructor with static members
interface ColorConstructor {
  new (r: number, g: number, b: number): Color;
  
  // Static methods
  fromHex(hex: string): Color;
  fromHSL(h: number, s: number, l: number): Color;
  
  // Static properties
  readonly BLACK: Color;
  readonly WHITE: Color;
  readonly RED: Color;
}

interface Color {
  r: number;
  g: number;
  b: number;
  toHex(): string;
  toHSL(): { h: number; s: number; l: number };
}

const ColorClass: ColorConstructor = class implements Color {
  constructor(public r: number, public g: number, public b: number) {}
  
  toHex(): string {
    const toHex = (n: number) => n.toString(16).padStart(2, '0');
    return `#${toHex(this.r)}${toHex(this.g)}${toHex(this.b)}`;
  }
  
  toHSL(): { h: number; s: number; l: number } {
    // Simplified HSL conversion
    const r = this.r / 255;
    const g = this.g / 255;
    const b = this.b / 255;
    
    const max = Math.max(r, g, b);
    const min = Math.min(r, g, b);
    const l = (max + min) / 2;
    
    let h = 0, s = 0;
    
    if (max !== min) {
      const d = max - min;
      s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
      
      switch (max) {
        case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
        case g: h = ((b - r) / d + 2) / 6; break;
        case b: h = ((r - g) / d + 4) / 6; break;
      }
    }
    
    return { h: h * 360, s: s * 100, l: l * 100 };
  }
  
  static fromHex(hex: string): Color {
    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    if (!result) throw new Error('Invalid hex color');
    
    return new this(
      parseInt(result[1], 16),
      parseInt(result[2], 16),
      parseInt(result[3], 16)
    );
  }
  
  static fromHSL(h: number, s: number, l: number): Color {
    // Simplified HSL to RGB conversion
    const hue2rgb = (p: number, q: number, t: number) => {
      if (t < 0) t += 1;
      if (t > 1) t -= 1;
      if (t < 1/6) return p + (q - p) * 6 * t;
      if (t < 1/2) return q;
      if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
      return p;
    };
    
    const hNorm = h / 360;
    const sNorm = s / 100;
    const lNorm = l / 100;
    
    let r, g, b;
    
    if (sNorm === 0) {
      r = g = b = lNorm;
    } else {
      const q = lNorm < 0.5 
        ? lNorm * (1 + sNorm) 
        : lNorm + sNorm - lNorm * sNorm;
      const p = 2 * lNorm - q;
      
      r = hue2rgb(p, q, hNorm + 1/3);
      g = hue2rgb(p, q, hNorm);
      b = hue2rgb(p, q, hNorm - 1/3);
    }
    
    return new this(
      Math.round(r * 255),
      Math.round(g * 255),
      Math.round(b * 255)
    );
  }
  
  static readonly BLACK = new this(0, 0, 0);
  static readonly WHITE = new this(255, 255, 255);
  static readonly RED = new this(255, 0, 0);
};

// 🏭 Factory pattern with constructor interfaces
interface ComponentConstructor<T extends Component> {
  new (props: any): T;
  displayName: string;
  defaultProps?: Partial<T['props']>;
}

interface Component {
  props: any;
  render(): string;
  update(newProps: any): void;
}

class ComponentFactory {
  private components = new Map<string, ComponentConstructor<any>>();
  
  register<T extends Component>(
    name: string,
    constructor: ComponentConstructor<T>
  ): void {
    this.components.set(name, constructor);
    console.log(`πŸ“¦ Registered component: ${name}`);
  }
  
  create<T extends Component>(
    name: string,
    props: any
  ): T {
    const Constructor = this.components.get(name);
    if (!Constructor) {
      throw new Error(`Component '${name}' not found`);
    }
    
    const mergedProps = {
      ...Constructor.defaultProps,
      ...props
    };
    
    return new Constructor(mergedProps) as T;
  }
  
  listComponents(): string[] {
    return Array.from(this.components.keys());
  }
}

🌟 Advanced Constructor Patterns

Complex constructor interfaces and patterns:

// 🎯 Generic constructor with constraints
interface Model {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}

interface ModelConstructor<T extends Model> {
  new (data: Partial<T>): T;
  
  // Static ORM-like methods
  find(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  create(data: Omit<T, keyof Model>): Promise<T>;
  update(id: string, data: Partial<T>): Promise<T>;
  delete(id: string): Promise<boolean>;
  
  // Schema information
  readonly tableName: string;
  readonly fields: Record<keyof T, FieldDefinition>;
}

interface FieldDefinition {
  type: 'string' | 'number' | 'boolean' | 'date';
  required: boolean;
  unique?: boolean;
  default?: any;
  validate?: (value: any) => boolean;
}

// πŸ—οΈ Builder pattern with constructable interface
interface BuilderConstructor<T, B extends Builder<T>> {
  new (): B;
}

interface Builder<T> {
  build(): T;
  reset(): this;
}

interface QueryBuilder extends Builder<string> {
  select(...fields: string[]): this;
  from(table: string): this;
  where(condition: string): this;
  orderBy(field: string, direction?: 'ASC' | 'DESC'): this;
  limit(count: number): this;
}

const SQLQueryBuilder: BuilderConstructor<string, QueryBuilder> = class implements QueryBuilder {
  private query: {
    select: string[];
    from: string;
    where: string[];
    orderBy: { field: string; direction: string }[];
    limit?: number;
  } = {
    select: [],
    from: '',
    where: [],
    orderBy: []
  };
  
  select(...fields: string[]): this {
    this.query.select.push(...fields);
    return this;
  }
  
  from(table: string): this {
    this.query.from = table;
    return this;
  }
  
  where(condition: string): this {
    this.query.where.push(condition);
    return this;
  }
  
  orderBy(field: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
    this.query.orderBy.push({ field, direction });
    return this;
  }
  
  limit(count: number): this {
    this.query.limit = count;
    return this;
  }
  
  build(): string {
    const parts: string[] = [];
    
    if (this.query.select.length > 0) {
      parts.push(`SELECT ${this.query.select.join(', ')}`);
    } else {
      parts.push('SELECT *');
    }
    
    if (this.query.from) {
      parts.push(`FROM ${this.query.from}`);
    }
    
    if (this.query.where.length > 0) {
      parts.push(`WHERE ${this.query.where.join(' AND ')}`);
    }
    
    if (this.query.orderBy.length > 0) {
      const orderClauses = this.query.orderBy
        .map(o => `${o.field} ${o.direction}`)
        .join(', ');
      parts.push(`ORDER BY ${orderClauses}`);
    }
    
    if (this.query.limit) {
      parts.push(`LIMIT ${this.query.limit}`);
    }
    
    return parts.join(' ');
  }
  
  reset(): this {
    this.query = {
      select: [],
      from: '',
      where: [],
      orderBy: []
    };
    return this;
  }
};

// πŸ”Œ Plugin system with constructable interfaces
interface PluginConstructor<T extends PluginBase> {
  new (options?: any): T;
  
  // Plugin metadata
  readonly pluginName: string;
  readonly version: string;
  readonly dependencies?: string[];
  
  // Static validation
  validateOptions?(options: any): boolean;
}

interface PluginBase {
  initialize(): Promise<void>;
  execute(context: any): Promise<void>;
  destroy(): Promise<void>;
}

class PluginLoader {
  private plugins = new Map<string, PluginBase>();
  private constructors = new Map<string, PluginConstructor<any>>();
  
  register<T extends PluginBase>(
    PluginClass: PluginConstructor<T>
  ): void {
    const name = PluginClass.pluginName;
    
    if (this.constructors.has(name)) {
      throw new Error(`Plugin '${name}' already registered`);
    }
    
    this.constructors.set(name, PluginClass);
    console.log(`πŸ”Œ Registered plugin: ${name} v${PluginClass.version}`);
  }
  
  async load(name: string, options?: any): Promise<void> {
    const PluginClass = this.constructors.get(name);
    
    if (!PluginClass) {
      throw new Error(`Plugin '${name}' not found`);
    }
    
    // Validate options if validator exists
    if (PluginClass.validateOptions && !PluginClass.validateOptions(options)) {
      throw new Error(`Invalid options for plugin '${name}'`);
    }
    
    // Check dependencies
    if (PluginClass.dependencies) {
      for (const dep of PluginClass.dependencies) {
        if (!this.plugins.has(dep)) {
          await this.load(dep);
        }
      }
    }
    
    // Create and initialize plugin
    const plugin = new PluginClass(options);
    await plugin.initialize();
    
    this.plugins.set(name, plugin);
    console.log(`βœ… Loaded plugin: ${name}`);
  }
  
  async executeAll(context: any): Promise<void> {
    for (const [name, plugin] of this.plugins) {
      console.log(`πŸ”§ Executing plugin: ${name}`);
      await plugin.execute(context);
    }
  }
}

// Example plugin implementation
const LoggerPlugin: PluginConstructor<PluginBase> = class implements PluginBase {
  static readonly pluginName = 'logger';
  static readonly version = '1.0.0';
  
  private logLevel: string;
  
  constructor(options?: { level?: string }) {
    this.logLevel = options?.level || 'info';
  }
  
  async initialize(): Promise<void> {
    console.log(`πŸ“ Logger initialized with level: ${this.logLevel}`);
  }
  
  async execute(context: any): Promise<void> {
    console.log(`[${this.logLevel.toUpperCase()}]`, context);
  }
  
  async destroy(): Promise<void> {
    console.log('πŸ“ Logger destroyed');
  }
  
  static validateOptions(options: any): boolean {
    if (!options) return true;
    
    const validLevels = ['debug', 'info', 'warn', 'error'];
    return !options.level || validLevels.includes(options.level);
  }
};

🎨 Hybrid Types: Combining Both

πŸ”€ Interfaces That Are Both Callable and Constructable

Creating interfaces with both capabilities:

// 🎯 Hybrid interface example
interface HybridFunction {
  // Call signatures
  (): void;
  (message: string): void;
  
  // Constructor signature
  new (config?: any): HybridInstance;
  
  // Static properties
  version: string;
  instances: HybridInstance[];
  
  // Static methods
  configure(options: any): void;
  getInstance(id: string): HybridInstance | undefined;
}

interface HybridInstance {
  id: string;
  execute(): void;
}

// Implementation
const createHybrid = (): HybridFunction => {
  const instances: HybridInstance[] = [];
  let config: any = {};
  
  // Create the hybrid function
  const hybrid = function(this: any, message?: string) {
    // Check if called with 'new'
    if (new.target) {
      // Constructor behavior
      const instance: HybridInstance = {
        id: `instance_${Date.now()}`,
        execute() {
          console.log(`πŸš€ Executing instance ${this.id}`);
        }
      };
      
      instances.push(instance);
      return instance;
    }
    
    // Regular function behavior
    if (message) {
      console.log(`πŸ“’ Message: ${message}`);
    } else {
      console.log('πŸ”” Default action executed');
    }
  } as any as HybridFunction;
  
  // Add static properties
  hybrid.version = '1.0.0';
  hybrid.instances = instances;
  
  // Add static methods
  hybrid.configure = (options: any) => {
    config = { ...config, ...options };
    console.log('βš™οΈ Configuration updated:', config);
  };
  
  hybrid.getInstance = (id: string) => {
    return instances.find(inst => inst.id === id);
  };
  
  return hybrid;
};

// Usage
const myHybrid = createHybrid();

// Use as function
myHybrid(); // Default action
myHybrid('Hello, World!'); // With message

// Use as constructor
const instance1 = new myHybrid();
const instance2 = new myHybrid({ name: 'Test' });

instance1.execute();
console.log('Total instances:', myHybrid.instances.length);

// πŸ—οΈ jQuery-like library interface
interface DOMQuery {
  // Call signature - selector function
  (selector: string): DOMQuery;
  (element: HTMLElement): DOMQuery;
  (callback: () => void): void; // Document ready
  
  // Constructor signature
  new (selector: string): DOMQuery;
  
  // jQuery-like methods (chainable)
  addClass(className: string): DOMQuery;
  removeClass(className: string): DOMQuery;
  on(event: string, handler: EventListener): DOMQuery;
  off(event: string, handler: EventListener): DOMQuery;
  css(property: string, value: string): DOMQuery;
  html(): string;
  html(content: string): DOMQuery;
  
  // Properties
  length: number;
  
  // Static methods (like $.ajax)
  ajax(options: AjaxOptions): Promise<any>;
  extend<T, U>(target: T, source: U): T & U;
  isArray(obj: any): obj is any[];
  
  // Plugin system
  fn: {
    [key: string]: (...args: any[]) => DOMQuery;
  };
}

interface AjaxOptions {
  url: string;
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  data?: any;
  headers?: Record<string, string>;
}

// Simplified implementation
const $ = (function(): DOMQuery {
  const DOMQueryImpl = function(this: any, selector: any) {
    // Handle different call signatures
    if (typeof selector === 'function') {
      // Document ready
      if (document.readyState === 'complete') {
        selector();
      } else {
        document.addEventListener('DOMContentLoaded', selector);
      }
      return;
    }
    
    // Constructor behavior
    if (new.target) {
      this.elements = typeof selector === 'string'
        ? document.querySelectorAll(selector)
        : [selector];
      this.length = this.elements.length;
      return this;
    }
    
    // Regular function call
    return new (DOMQueryImpl as any)(selector);
  } as any as DOMQuery;
  
  // Chainable methods
  DOMQueryImpl.prototype.addClass = function(className: string) {
    this.elements.forEach((el: HTMLElement) => el.classList.add(className));
    return this;
  };
  
  DOMQueryImpl.prototype.removeClass = function(className: string) {
    this.elements.forEach((el: HTMLElement) => el.classList.remove(className));
    return this;
  };
  
  DOMQueryImpl.prototype.on = function(event: string, handler: EventListener) {
    this.elements.forEach((el: HTMLElement) => el.addEventListener(event, handler));
    return this;
  };
  
  DOMQueryImpl.prototype.html = function(content?: string) {
    if (content === undefined) {
      return this.elements[0]?.innerHTML || '';
    }
    this.elements.forEach((el: HTMLElement) => el.innerHTML = content);
    return this;
  };
  
  // Static methods
  DOMQueryImpl.ajax = async (options: AjaxOptions) => {
    const response = await fetch(options.url, {
      method: options.method || 'GET',
      headers: options.headers,
      body: options.data ? JSON.stringify(options.data) : undefined
    });
    return response.json();
  };
  
  DOMQueryImpl.extend = <T, U>(target: T, source: U): T & U => {
    return Object.assign({}, target, source);
  };
  
  DOMQueryImpl.isArray = Array.isArray;
  
  // Plugin system
  DOMQueryImpl.fn = DOMQueryImpl.prototype;
  
  return DOMQueryImpl;
})();

// Usage examples
$(() => {
  console.log('Document ready!');
});

// Both work - as function and constructor
const elements1 = $('.my-class');
const elements2 = new $('.my-class');

elements1
  .addClass('active')
  .on('click', () => console.log('Clicked!'))
  .css('color', 'blue');

⚠️ Common Pitfalls and Solutions

😱 Pitfall 1: Type Inference Issues

// ❌ Problem - TypeScript can't infer callable interface
const badCallable = (name: string) => {
  return `Hello, ${name}`;
};

// Trying to add properties doesn't work
badCallable.language = 'en'; // Error! Property doesn't exist

// βœ… Solution 1 - Explicit typing
interface Greeter {
  (name: string): string;
  language: string;
}

const goodCallable: Greeter = Object.assign(
  (name: string) => `Hello, ${name}`,
  { language: 'en' }
);

// βœ… Solution 2 - Factory function
function createCallable(): Greeter {
  const fn = (name: string) => `Hello, ${name}`;
  (fn as any).language = 'en';
  return fn as Greeter;
}

// βœ… Solution 3 - Class-based approach
class CallableClass {
  language = 'en';
  
  constructor() {
    const callable = (name: string) => `Hello, ${name}`;
    Object.setPrototypeOf(callable, CallableClass.prototype);
    Object.assign(callable, this);
    return callable as any;
  }
}

🀯 Pitfall 2: Constructor Type Compatibility

// ❌ Problem - Constructor parameter mismatch
interface AnimalConstructor {
  new (name: string): Animal;
}

interface Animal {
  name: string;
}

class Dog implements Animal {
  constructor(public name: string, public breed: string) {} // Extra parameter!
}

// const DogConstructor: AnimalConstructor = Dog; // Error!

// βœ… Solution 1 - Make constructor more flexible
interface FlexibleConstructor<T> {
  new (...args: any[]): T;
}

const DogConstructor1: FlexibleConstructor<Animal> = Dog; // OK

// βœ… Solution 2 - Use factory pattern
interface AnimalFactory<T extends Animal> {
  create(name: string): T;
}

class DogFactory implements AnimalFactory<Dog> {
  create(name: string): Dog {
    return new Dog(name, 'Unknown');
  }
}

// βœ… Solution 3 - Adapter pattern
const createAnimalConstructor = <T extends Animal>(
  Constructor: new (...args: any[]) => T,
  ...defaultArgs: any[]
): AnimalConstructor => {
  return class {
    constructor(name: string) {
      return new Constructor(name, ...defaultArgs);
    }
  } as any;
};

const AdaptedDog = createAnimalConstructor(Dog, 'Mixed');

πŸ› οΈ Best Practices

🎯 Design Guidelines

  1. Clear Intent 🎯: Make it obvious when something is callable/constructable
  2. Type Safety πŸ›‘οΈ: Always provide explicit types for complex interfaces
  3. Documentation πŸ“: Document the expected behavior clearly
  4. Consistency πŸ”„: Use consistent patterns across your codebase
// 🌟 Well-designed callable interface
interface WellDesignedCallable {
  // Clear call signatures with JSDoc
  /** 
   * Execute the main function
   * @param input - The input to process
   * @returns Processed result
   */
  (input: string): string;
  
  /**
   * Execute with options
   * @param input - The input to process
   * @param options - Processing options
   */
  (input: string, options: ProcessOptions): ProcessedResult;
  
  // Well-organized properties
  readonly version: string;
  readonly name: string;
  
  // Configuration
  config: {
    timeout: number;
    retries: number;
    debug: boolean;
  };
  
  // Clear method names
  configure(options: Partial<ProcessOptions>): void;
  reset(): void;
  
  // Event handling
  on(event: 'start' | 'complete' | 'error', handler: Function): void;
  off(event: string, handler: Function): void;
}

interface ProcessOptions {
  mode: 'fast' | 'accurate';
  encoding?: string;
  validate?: boolean;
}

interface ProcessedResult {
  output: string;
  metadata: {
    processTime: number;
    inputLength: number;
    outputLength: number;
  };
}

// πŸ—οΈ Well-designed constructor interface
interface WellDesignedConstructor<T, O = any> {
  // Clear constructor signature
  new (options?: O): T;
  
  // Factory methods with descriptive names
  create(options?: O): T;
  createDefault(): T;
  createFrom<U>(source: U, transformer: (source: U) => O): T;
  
  // Validation
  validate(instance: any): instance is T;
  isValidOptions(options: any): options is O;
  
  // Metadata
  readonly className: string;
  readonly version: string;
  readonly schema?: Schema<T>;
}

interface Schema<T> {
  fields: {
    [K in keyof T]: FieldSchema;
  };
  validate(data: any): ValidationResult;
}

interface FieldSchema {
  type: string;
  required: boolean;
  validator?: (value: any) => boolean;
}

interface ValidationResult {
  valid: boolean;
  errors?: string[];
}

πŸ§ͺ Hands-On Exercise

🎯 Challenge: Build a Command System

Create a flexible command system using callable and constructable interfaces:

πŸ“‹ Requirements:

  • βœ… Commands are both callable and have properties
  • 🎨 Support command chaining
  • 🎯 Implement undo/redo functionality
  • πŸ“Š Track command history
  • πŸ”§ Plugin-based command extensions

πŸš€ Bonus Points:

  • Add command validation
  • Implement command macros
  • Create async command support

πŸ’‘ Solution

πŸ” Click to see solution
// 🎯 Command system with callable and constructable interfaces

// Command interfaces
interface Command<T = any, R = any> {
  // Callable - execute the command
  (context: T): R | Promise<R>;
  
  // Properties
  readonly name: string;
  readonly description: string;
  readonly category: string;
  
  // Configuration
  config: CommandConfig;
  
  // Methods
  canExecute(context: T): boolean;
  validate?(args: any[]): boolean;
  undo?(context: T): void;
  
  // Metadata
  metadata: {
    executionCount: number;
    lastExecuted?: Date;
    averageTime?: number;
  };
}

interface CommandConfig {
  async: boolean;
  undoable: boolean;
  requiresConfirmation: boolean;
  timeout?: number;
  retries?: number;
}

interface CommandConstructor<T = any, R = any> {
  new (options?: Partial<CommandConfig>): Command<T, R>;
  
  commandName: string;
  category: string;
  description: string;
  defaultConfig: CommandConfig;
}

// Command history entry
interface HistoryEntry<T = any, R = any> {
  command: Command<T, R>;
  context: T;
  result?: R;
  timestamp: Date;
  duration: number;
  status: 'success' | 'failed' | 'cancelled';
  error?: Error;
}

// Command manager
class CommandManager<T = any> {
  private commands = new Map<string, Command<T>>();
  private history: HistoryEntry<T>[] = [];
  private historyIndex = -1;
  private constructors = new Map<string, CommandConstructor<T>>();
  
  // Register command instance
  register(command: Command<T>): void {
    this.commands.set(command.name, command);
    console.log(`πŸ“ Registered command: ${command.name}`);
  }
  
  // Register command constructor
  registerConstructor(Constructor: CommandConstructor<T>): void {
    this.constructors.set(Constructor.commandName, Constructor);
    console.log(`πŸ—οΈ Registered command constructor: ${Constructor.commandName}`);
  }
  
  // Create command from constructor
  createCommand(name: string, options?: Partial<CommandConfig>): Command<T> {
    const Constructor = this.constructors.get(name);
    if (!Constructor) {
      throw new Error(`Command constructor '${name}' not found`);
    }
    
    return new Constructor(options);
  }
  
  // Execute command
  async execute(commandName: string, context: T): Promise<any> {
    const command = this.commands.get(commandName);
    if (!command) {
      throw new Error(`Command '${commandName}' not found`);
    }
    
    // Check if can execute
    if (!command.canExecute(context)) {
      throw new Error(`Command '${commandName}' cannot be executed in current context`);
    }
    
    // Confirm if required
    if (command.config.requiresConfirmation) {
      console.log(`⚠️ Command '${commandName}' requires confirmation`);
      // In real app, would show confirmation dialog
    }
    
    // Execute with timing
    const startTime = Date.now();
    let result: any;
    let status: HistoryEntry['status'] = 'success';
    let error: Error | undefined;
    
    try {
      // Handle timeout
      if (command.config.timeout) {
        result = await Promise.race([
          command(context),
          new Promise((_, reject) => 
            setTimeout(() => reject(new Error('Command timeout')), command.config.timeout)
          )
        ]);
      } else {
        result = await command(context);
      }
      
      // Update metadata
      command.metadata.executionCount++;
      command.metadata.lastExecuted = new Date();
      
    } catch (err) {
      status = 'failed';
      error = err as Error;
      console.error(`❌ Command '${commandName}' failed:`, err);
      
      // Retry if configured
      if (command.config.retries && command.config.retries > 0) {
        console.log(`πŸ”„ Retrying command (${command.config.retries} attempts left)`);
        // Implement retry logic
      }
      
      throw err;
    }
    
    const duration = Date.now() - startTime;
    
    // Update average time
    const prevAvg = command.metadata.averageTime || 0;
    const count = command.metadata.executionCount;
    command.metadata.averageTime = (prevAvg * (count - 1) + duration) / count;
    
    // Add to history
    const entry: HistoryEntry<T> = {
      command,
      context: this.cloneContext(context),
      result,
      timestamp: new Date(),
      duration,
      status,
      error
    };
    
    // Truncate forward history if we're not at the end
    if (this.historyIndex < this.history.length - 1) {
      this.history = this.history.slice(0, this.historyIndex + 1);
    }
    
    this.history.push(entry);
    this.historyIndex = this.history.length - 1;
    
    console.log(`βœ… Executed '${commandName}' in ${duration}ms`);
    return result;
  }
  
  // Undo last command
  async undo(): Promise<boolean> {
    if (this.historyIndex < 0) {
      console.log('❌ Nothing to undo');
      return false;
    }
    
    const entry = this.history[this.historyIndex];
    if (!entry.command.config.undoable || !entry.command.undo) {
      console.log(`❌ Command '${entry.command.name}' is not undoable`);
      return false;
    }
    
    try {
      await entry.command.undo(entry.context);
      this.historyIndex--;
      console.log(`↩️ Undid '${entry.command.name}'`);
      return true;
    } catch (error) {
      console.error('❌ Undo failed:', error);
      return false;
    }
  }
  
  // Redo command
  async redo(): Promise<boolean> {
    if (this.historyIndex >= this.history.length - 1) {
      console.log('❌ Nothing to redo');
      return false;
    }
    
    this.historyIndex++;
    const entry = this.history[this.historyIndex];
    
    try {
      await entry.command(entry.context);
      console.log(`β†ͺ️ Redid '${entry.command.name}'`);
      return true;
    } catch (error) {
      console.error('❌ Redo failed:', error);
      this.historyIndex--;
      return false;
    }
  }
  
  // Get command history
  getHistory(): HistoryEntry<T>[] {
    return [...this.history];
  }
  
  // Create command chain
  chain(...commandNames: string[]): ChainedCommand<T> {
    return new ChainedCommand(this, commandNames);
  }
  
  // Create macro
  macro(name: string, ...commandNames: string[]): void {
    const macroCommand = this.createMacroCommand(name, commandNames);
    this.register(macroCommand);
  }
  
  private createMacroCommand(name: string, commandNames: string[]): Command<T> {
    const manager = this;
    
    const macro = (async function(context: T) {
      const results = [];
      for (const cmdName of commandNames) {
        const result = await manager.execute(cmdName, context);
        results.push(result);
      }
      return results;
    }) as Command<T>;
    
    macro.name = name;
    macro.description = `Macro: ${commandNames.join(' -> ')}`;
    macro.category = 'macro';
    macro.config = {
      async: true,
      undoable: false,
      requiresConfirmation: false
    };
    
    macro.canExecute = (context: T) => {
      return commandNames.every(cmdName => {
        const cmd = manager.commands.get(cmdName);
        return cmd && cmd.canExecute(context);
      });
    };
    
    macro.metadata = {
      executionCount: 0
    };
    
    return macro;
  }
  
  private cloneContext(context: T): T {
    return JSON.parse(JSON.stringify(context));
  }
}

// Chained command builder
class ChainedCommand<T> {
  constructor(
    private manager: CommandManager<T>,
    private commandNames: string[]
  ) {}
  
  then(commandName: string): ChainedCommand<T> {
    this.commandNames.push(commandName);
    return this;
  }
  
  async execute(context: T): Promise<any[]> {
    const results = [];
    for (const commandName of this.commandNames) {
      const result = await this.manager.execute(commandName, context);
      results.push(result);
    }
    return results;
  }
}

// Example command implementations
interface AppContext {
  data: any[];
  selectedItems: any[];
  clipboard: any[];
  user: { name: string; role: string };
}

// Create base command class
abstract class BaseCommand implements Command<AppContext> {
  readonly name: string;
  readonly description: string;
  readonly category: string;
  config: CommandConfig;
  metadata = { executionCount: 0 };
  
  constructor(name: string, description: string, category: string, config?: Partial<CommandConfig>) {
    this.name = name;
    this.description = description;
    this.category = category;
    this.config = {
      async: false,
      undoable: false,
      requiresConfirmation: false,
      ...config
    };
  }
  
  abstract execute(context: AppContext): any;
  
  canExecute(context: AppContext): boolean {
    return true;
  }
  
  // Make callable
  [Symbol.for('nodejs.util.inspect.custom')]() {
    return `Command(${this.name})`;
  }
}

// Copy command
const CopyCommand: CommandConstructor<AppContext> = class extends BaseCommand {
  static commandName = 'copy';
  static category = 'edit';
  static description = 'Copy selected items to clipboard';
  static defaultConfig: CommandConfig = {
    async: false,
    undoable: false,
    requiresConfirmation: false
  };
  
  constructor(options?: Partial<CommandConfig>) {
    super(
      CopyCommand.commandName,
      CopyCommand.description,
      CopyCommand.category,
      { ...CopyCommand.defaultConfig, ...options }
    );
    
    // Make instance callable
    const callable = this.execute.bind(this);
    Object.setPrototypeOf(callable, CopyCommand.prototype);
    Object.assign(callable, this);
    return callable as any;
  }
  
  execute(context: AppContext): void {
    context.clipboard = [...context.selectedItems];
    console.log(`πŸ“‹ Copied ${context.clipboard.length} items`);
  }
  
  canExecute(context: AppContext): boolean {
    return context.selectedItems.length > 0;
  }
};

// Paste command with undo
const PasteCommand: CommandConstructor<AppContext> = class extends BaseCommand {
  static commandName = 'paste';
  static category = 'edit';
  static description = 'Paste items from clipboard';
  static defaultConfig: CommandConfig = {
    async: false,
    undoable: true,
    requiresConfirmation: false
  };
  
  private pastedItems: any[] = [];
  
  constructor(options?: Partial<CommandConfig>) {
    super(
      PasteCommand.commandName,
      PasteCommand.description,
      PasteCommand.category,
      { ...PasteCommand.defaultConfig, ...options }
    );
    
    const callable = this.execute.bind(this);
    Object.setPrototypeOf(callable, PasteCommand.prototype);
    Object.assign(callable, this);
    return callable as any;
  }
  
  execute(context: AppContext): void {
    this.pastedItems = [...context.clipboard];
    context.data.push(...this.pastedItems);
    console.log(`πŸ“‹ Pasted ${this.pastedItems.length} items`);
  }
  
  canExecute(context: AppContext): boolean {
    return context.clipboard.length > 0;
  }
  
  undo(context: AppContext): void {
    // Remove pasted items
    this.pastedItems.forEach(item => {
      const index = context.data.indexOf(item);
      if (index > -1) {
        context.data.splice(index, 1);
      }
    });
    console.log(`↩️ Undid paste of ${this.pastedItems.length} items`);
    this.pastedItems = [];
  }
};

// Delete command with confirmation
const DeleteCommand: CommandConstructor<AppContext> = class extends BaseCommand {
  static commandName = 'delete';
  static category = 'edit';
  static description = 'Delete selected items';
  static defaultConfig: CommandConfig = {
    async: false,
    undoable: true,
    requiresConfirmation: true
  };
  
  private deletedItems: Array<{ item: any; index: number }> = [];
  
  constructor(options?: Partial<CommandConfig>) {
    super(
      DeleteCommand.commandName,
      DeleteCommand.description,
      DeleteCommand.category,
      { ...DeleteCommand.defaultConfig, ...options }
    );
    
    const callable = this.execute.bind(this);
    Object.setPrototypeOf(callable, DeleteCommand.prototype);
    Object.assign(callable, this);
    return callable as any;
  }
  
  execute(context: AppContext): void {
    this.deletedItems = [];
    
    context.selectedItems.forEach(item => {
      const index = context.data.indexOf(item);
      if (index > -1) {
        this.deletedItems.push({ item, index });
        context.data.splice(index, 1);
      }
    });
    
    context.selectedItems = [];
    console.log(`πŸ—‘οΈ Deleted ${this.deletedItems.length} items`);
  }
  
  canExecute(context: AppContext): boolean {
    return context.selectedItems.length > 0 && 
           context.user.role !== 'viewer';
  }
  
  undo(context: AppContext): void {
    // Restore deleted items at their original positions
    this.deletedItems
      .sort((a, b) => a.index - b.index)
      .forEach(({ item, index }) => {
        context.data.splice(index, 0, item);
      });
    
    console.log(`↩️ Restored ${this.deletedItems.length} items`);
    this.deletedItems = [];
  }
};

// Usage example
const manager = new CommandManager<AppContext>();

// Register constructors
manager.registerConstructor(CopyCommand);
manager.registerConstructor(PasteCommand);
manager.registerConstructor(DeleteCommand);

// Create and register command instances
const copyCmd = manager.createCommand('copy');
const pasteCmd = manager.createCommand('paste');
const deleteCmd = manager.createCommand('delete', { requiresConfirmation: false });

manager.register(copyCmd);
manager.register(pasteCmd);
manager.register(deleteCmd);

// Create macro
manager.macro('cut', 'copy', 'delete');

// Test context
const context: AppContext = {
  data: ['Item 1', 'Item 2', 'Item 3'],
  selectedItems: ['Item 2'],
  clipboard: [],
  user: { name: 'John', role: 'admin' }
};

// Execute commands
async function demo() {
  console.log('Initial data:', context.data);
  
  // Copy
  await manager.execute('copy', context);
  
  // Delete
  await manager.execute('delete', context);
  console.log('After delete:', context.data);
  
  // Paste
  context.selectedItems = [];
  await manager.execute('paste', context);
  console.log('After paste:', context.data);
  
  // Undo paste
  await manager.undo();
  console.log('After undo paste:', context.data);
  
  // Redo paste
  await manager.redo();
  console.log('After redo paste:', context.data);
  
  // Chain commands
  context.selectedItems = ['Item 1'];
  await manager.chain('copy', 'delete', 'paste').execute(context);
  console.log('After chain:', context.data);
  
  // Show history
  console.log('\nπŸ“œ Command History:');
  manager.getHistory().forEach(entry => {
    console.log(`- ${entry.command.name} (${entry.status}) - ${entry.duration}ms`);
  });
}

demo();

πŸŽ“ Key Takeaways

You now understand how to create powerful callable and constructable interfaces! Here’s what you’ve learned:

  • βœ… Callable interfaces define function signatures with properties 🎯
  • βœ… Constructable interfaces describe constructor signatures πŸ—οΈ
  • βœ… Hybrid types combine both capabilities πŸ”€
  • βœ… Factory patterns work great with these interfaces 🏭
  • βœ… Real-world applications like jQuery-style APIs ✨

Remember: Functions in JavaScript are objects too - TypeScript’s callable interfaces let you fully express this power! πŸš€

🀝 Next Steps

Congratulations! πŸŽ‰ You’ve mastered callable and constructable interfaces!

Here’s what to do next:

  1. πŸ’» Practice with the command system exercise above
  2. πŸ—οΈ Create your own callable APIs
  3. πŸ“š Move on to our next tutorial: Hybrid Types: Combining Multiple Type Kinds
  4. 🌟 Apply these patterns to build powerful, flexible APIs!

Remember: The best APIs are both powerful and intuitive. Keep building! πŸš€


Happy coding! πŸŽ‰πŸš€βœ¨