+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 48 of 354

🎭 Hybrid Types: Combining Multiple Type Kinds

Master hybrid types in TypeScript to create objects that are simultaneously functions, constructors, and have properties 🚀

💎Advanced
30 min read

Prerequisites

  • Understanding of callable interfaces 📝
  • Knowledge of constructable interfaces 🔍
  • Interface and type system mastery 💻

What you'll learn

  • Create hybrid types combining multiple behaviors 🎯
  • Design flexible library APIs 🏗️
  • Implement jQuery-style patterns 🛡️
  • Build powerful utility functions ✨

🎯 Introduction

Welcome to the fascinating world of hybrid types! 🎉 In this guide, we’ll explore how TypeScript allows you to create objects that combine multiple type behaviors - objects that can be called as functions, instantiated as constructors, and still have properties and methods.

You’ll discover how hybrid types are like Swiss Army knives 🔧 - versatile tools that adapt to different situations. Whether you’re building jQuery-style libraries 💰, creating fluent APIs 🌊, or designing plugin systems 🔌, understanding hybrid types opens up powerful architectural patterns.

By the end of this tutorial, you’ll be confidently creating objects that blur the lines between functions, constructors, and regular objects! Let’s explore this type flexibility! 🏊‍♂️

📚 Understanding Hybrid Types

🤔 What are Hybrid Types?

Hybrid types are TypeScript’s way of representing JavaScript objects that exhibit multiple behaviors. Since JavaScript functions are objects, they can have properties, be called, and even be used as constructors - all at the same time!

Think of hybrid types like:

  • 🎭 Theater actors: Playing multiple roles simultaneously
  • 🔧 Multi-tools: One tool, many functions
  • 🎪 Circus performers: Juggling different acts
  • 🦸 Superheroes: Multiple powers in one person

💡 Real-World Examples

Common hybrid patterns you’ve probably used:

  1. jQuery 💰: $() is a function, but $.ajax is a method
  2. Express 🚂: app() creates middleware, but app.get() defines routes
  3. Lodash 🔧: _() wraps values, but _.map is a utility
  4. Moment.js ⏰: moment() creates dates, but moment.utc() is a factory

🔧 Building Hybrid Types

📝 Basic Hybrid Type Pattern

Let’s start with fundamental hybrid type creation:

// 🎯 Hybrid type interface
interface Counter {
  // Callable signature
  (): number;
  
  // Properties
  count: number;
  
  // Methods
  increment(): void;
  decrement(): void;
  reset(): void;
  
  // Nested functionality
  history: {
    values: number[];
    max(): number;
    min(): number;
    average(): number;
  };
}

// 🏗️ Creating a hybrid counter
const createCounter = (initialValue: number = 0): Counter => {
  let count = initialValue;
  const history: number[] = [initialValue];
  
  // Create the hybrid function
  const counter = (() => {
    return count;
  }) as Counter;
  
  // Add properties
  counter.count = count;
  
  // Add methods
  counter.increment = () => {
    count++;
    counter.count = count;
    history.push(count);
  };
  
  counter.decrement = () => {
    count--;
    counter.count = count;
    history.push(count);
  };
  
  counter.reset = () => {
    count = initialValue;
    counter.count = count;
    history.push(count);
  };
  
  // Add nested functionality
  counter.history = {
    values: history,
    max: () => Math.max(...history),
    min: () => Math.min(...history),
    average: () => history.reduce((a, b) => a + b, 0) / history.length
  };
  
  return counter;
};

// 💫 Using the hybrid counter
const counter = createCounter(10);

console.log(counter()); // 10 - callable
console.log(counter.count); // 10 - property

counter.increment();
console.log(counter()); // 11
console.log(counter.count); // 11

counter.increment();
counter.increment();
console.log(counter.history.max()); // 13
console.log(counter.history.average()); // 11.5

🎭 Advanced Hybrid Patterns

Let’s create more sophisticated hybrid types:

// 🌊 jQuery-style hybrid type
interface DOMQuery {
  // Callable - selects elements
  (selector: string): DOMQuery;
  
  // Array-like access
  [index: number]: HTMLElement;
  length: number;
  
  // Chainable methods
  addClass(className: string): DOMQuery;
  removeClass(className: string): DOMQuery;
  toggleClass(className: string): DOMQuery;
  
  // Properties
  version: string;
  
  // Static-like methods
  ajax(options: AjaxOptions): Promise<any>;
  extend<T, U>(target: T, source: U): T & U;
  
  // Event handling
  on(event: string, handler: EventHandler): DOMQuery;
  off(event: string, handler?: EventHandler): DOMQuery;
  trigger(event: string, data?: any): DOMQuery;
}

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

type EventHandler = (event: Event, data?: any) => void;

// 🏗️ Create jQuery-like implementation
const createDOMQuery = (): DOMQuery => {
  const eventHandlers = new Map<string, Set<EventHandler>>();
  
  const $ = ((selector: string) => {
    const elements = document.querySelectorAll(selector);
    const result = Object.create($.prototype) as DOMQuery;
    
    // Make it array-like
    Array.from(elements).forEach((el, i) => {
      result[i] = el as HTMLElement;
    });
    result.length = elements.length;
    
    return result;
  }) as DOMQuery;
  
  // Add version
  $.version = '3.0.0';
  
  // Chainable methods
  const proto = {
    addClass(className: string): DOMQuery {
      for (let i = 0; i < this.length; i++) {
        this[i].classList.add(className);
      }
      return this;
    },
    
    removeClass(className: string): DOMQuery {
      for (let i = 0; i < this.length; i++) {
        this[i].classList.remove(className);
      }
      return this;
    },
    
    toggleClass(className: string): DOMQuery {
      for (let i = 0; i < this.length; i++) {
        this[i].classList.toggle(className);
      }
      return this;
    },
    
    on(event: string, handler: EventHandler): DOMQuery {
      if (!eventHandlers.has(event)) {
        eventHandlers.set(event, new Set());
      }
      eventHandlers.get(event)!.add(handler);
      
      for (let i = 0; i < this.length; i++) {
        this[i].addEventListener(event, handler as any);
      }
      return this;
    },
    
    off(event: string, handler?: EventHandler): DOMQuery {
      if (handler) {
        eventHandlers.get(event)?.delete(handler);
        for (let i = 0; i < this.length; i++) {
          this[i].removeEventListener(event, handler as any);
        }
      } else {
        const handlers = eventHandlers.get(event);
        if (handlers) {
          handlers.forEach(h => {
            for (let i = 0; i < this.length; i++) {
              this[i].removeEventListener(event, h as any);
            }
          });
          handlers.clear();
        }
      }
      return this;
    },
    
    trigger(event: string, data?: any): DOMQuery {
      const customEvent = new CustomEvent(event, { detail: data });
      for (let i = 0; i < this.length; i++) {
        this[i].dispatchEvent(customEvent);
      }
      return this;
    }
  };
  
  // Static methods
  $.ajax = async (options: AjaxOptions): Promise<any> => {
    const response = await fetch(options.url, {
      method: options.method || 'GET',
      headers: options.headers,
      body: options.data ? JSON.stringify(options.data) : undefined
    });
    return response.json();
  };
  
  $.extend = <T, U>(target: T, source: U): T & U => {
    return Object.assign({}, target, source);
  };
  
  // Set up prototype chain
  Object.setPrototypeOf($, proto);
  
  return $;
};

// 💫 Usage example
const $ = createDOMQuery();

// Function call
$('.button')
  .addClass('primary')
  .on('click', (e) => console.log('Clicked!'))
  .toggleClass('active');

// Static method
$.ajax({
  url: '/api/data',
  method: 'GET'
}).then(data => console.log(data));

// Property access
console.log($.version); // "3.0.0"

🚀 Complex Hybrid Type Patterns

🎨 Plugin System with Hybrid Types

Let’s create a sophisticated plugin system:

// 🔌 Plugin system interfaces
interface Plugin<T = any> {
  // Callable - initializes plugin
  (options?: T): PluginInstance;
  
  // Constructor - creates new instances
  new (element: HTMLElement, options?: T): PluginInstance;
  
  // Metadata
  name: string;
  version: string;
  author: string;
  
  // Static methods
  defaults: T;
  setDefaults(defaults: Partial<T>): void;
  
  // Plugin registry
  instances: WeakMap<HTMLElement, PluginInstance>;
  getInstance(element: HTMLElement): PluginInstance | undefined;
  
  // Lifecycle hooks
  hooks: {
    beforeCreate?: (options: T) => void;
    created?: (instance: PluginInstance) => void;
    beforeDestroy?: (instance: PluginInstance) => void;
    destroyed?: () => void;
  };
}

interface PluginInstance {
  element: HTMLElement;
  options: any;
  destroy(): void;
  update(options: any): void;
}

// 🏗️ Plugin factory
const createPlugin = <T extends object>(
  name: string,
  defaultOptions: T,
  implementation: (element: HTMLElement, options: T) => PluginInstance
): Plugin<T> => {
  const instances = new WeakMap<HTMLElement, PluginInstance>();
  let defaults = { ...defaultOptions };
  
  // Create the hybrid type
  const PluginConstructor = function(
    this: any,
    elementOrOptions?: HTMLElement | T,
    options?: T
  ) {
    // Handle both callable and constructor patterns
    if (this instanceof PluginConstructor) {
      // Called with 'new'
      const element = elementOrOptions as HTMLElement;
      const opts = { ...defaults, ...options };
      
      // Lifecycle hook
      PluginConstructor.hooks.beforeCreate?.(opts);
      
      const instance = implementation(element, opts);
      instances.set(element, instance);
      
      // Lifecycle hook
      PluginConstructor.hooks.created?.(instance);
      
      return instance;
    } else {
      // Called as function
      const opts = { ...defaults, ...(elementOrOptions as T) };
      return (element: HTMLElement) => new PluginConstructor(element, opts);
    }
  } as any as Plugin<T>;
  
  // Add metadata
  PluginConstructor.name = name;
  PluginConstructor.version = '1.0.0';
  PluginConstructor.author = 'TypeScript Master';
  
  // Add static properties and methods
  PluginConstructor.defaults = defaults;
  PluginConstructor.setDefaults = (newDefaults: Partial<T>) => {
    defaults = { ...defaults, ...newDefaults };
    PluginConstructor.defaults = defaults;
  };
  
  PluginConstructor.instances = instances;
  PluginConstructor.getInstance = (element: HTMLElement) => {
    return instances.get(element);
  };
  
  PluginConstructor.hooks = {};
  
  return PluginConstructor;
};

// 💡 Example: Tooltip Plugin
interface TooltipOptions {
  content: string;
  position: 'top' | 'bottom' | 'left' | 'right';
  delay: number;
  animation: boolean;
  theme: 'light' | 'dark';
}

const Tooltip = createPlugin<TooltipOptions>(
  'Tooltip',
  {
    content: '',
    position: 'top',
    delay: 200,
    animation: true,
    theme: 'light'
  },
  (element, options) => {
    let tooltipEl: HTMLElement | null = null;
    let showTimeout: number;
    let hideTimeout: number;
    
    const show = () => {
      clearTimeout(hideTimeout);
      showTimeout = window.setTimeout(() => {
        if (!tooltipEl) {
          tooltipEl = document.createElement('div');
          tooltipEl.className = `tooltip tooltip-${options.theme} tooltip-${options.position}`;
          tooltipEl.textContent = options.content;
          if (options.animation) {
            tooltipEl.classList.add('tooltip-animated');
          }
          document.body.appendChild(tooltipEl);
          
          // Position tooltip
          const rect = element.getBoundingClientRect();
          // Positioning logic here...
        }
      }, options.delay);
    };
    
    const hide = () => {
      clearTimeout(showTimeout);
      hideTimeout = window.setTimeout(() => {
        if (tooltipEl) {
          tooltipEl.remove();
          tooltipEl = null;
        }
      }, 100);
    };
    
    // Attach event listeners
    element.addEventListener('mouseenter', show);
    element.addEventListener('mouseleave', hide);
    
    return {
      element,
      options,
      destroy() {
        element.removeEventListener('mouseenter', show);
        element.removeEventListener('mouseleave', hide);
        if (tooltipEl) {
          tooltipEl.remove();
        }
        Tooltip.instances.delete(element);
      },
      update(newOptions: Partial<TooltipOptions>) {
        Object.assign(options, newOptions);
      }
    };
  }
);

// 💫 Using the plugin
// As constructor
const button = document.querySelector('.button')!;
const tooltip1 = new Tooltip(button, {
  content: 'Click me!',
  position: 'top',
  delay: 300,
  animation: true,
  theme: 'dark'
});

// As function (curried)
const darkTooltip = Tooltip({ theme: 'dark', position: 'bottom' });
const tooltip2 = darkTooltip(document.querySelector('.icon')!);

// Access static properties
console.log(Tooltip.name); // "Tooltip"
console.log(Tooltip.version); // "1.0.0"

// Set defaults
Tooltip.setDefaults({ theme: 'dark' });

// Get instance
const instance = Tooltip.getInstance(button);
console.log(instance === tooltip1); // true

🎯 Real-World Applications

🏗️ Framework Core with Hybrid Types

Let’s build a mini framework core:

// 🌐 Framework core interfaces
interface Framework {
  // Callable - creates app instance
  (config: AppConfig): App;
  
  // Constructor - alternative instantiation
  new (config: AppConfig): App;
  
  // Framework info
  version: string;
  name: string;
  
  // Global configuration
  config: GlobalConfig;
  configure(options: Partial<GlobalConfig>): void;
  
  // Plugin system
  use<T>(plugin: Plugin<T>, options?: T): Framework;
  plugins: Map<string, Plugin<any>>;
  
  // Component registry
  component(name: string, definition: ComponentDefinition): Framework;
  components: Map<string, ComponentDefinition>;
  
  // Directive registry
  directive(name: string, definition: DirectiveDefinition): Framework;
  directives: Map<string, DirectiveDefinition>;
  
  // Global API
  nextTick(callback: () => void): Promise<void>;
  observable<T>(value: T): Observable<T>;
  computed<T>(getter: () => T): ComputedRef<T>;
}

interface AppConfig {
  el: string | HTMLElement;
  data?: Record<string, any>;
  methods?: Record<string, Function>;
  computed?: Record<string, () => any>;
  mounted?: () => void;
  destroyed?: () => void;
}

interface App {
  $el: HTMLElement;
  $data: Record<string, any>;
  $mount(el?: string | HTMLElement): void;
  $destroy(): void;
  $on(event: string, handler: Function): void;
  $off(event: string, handler?: Function): void;
  $emit(event: string, ...args: any[]): void;
}

interface GlobalConfig {
  debug: boolean;
  performance: boolean;
  devtools: boolean;
  silent: boolean;
}

interface ComponentDefinition {
  template?: string;
  props?: string[];
  data?: () => Record<string, any>;
  methods?: Record<string, Function>;
  computed?: Record<string, () => any>;
  mounted?: () => void;
  destroyed?: () => void;
}

interface DirectiveDefinition {
  bind?: (el: HTMLElement, binding: any) => void;
  update?: (el: HTMLElement, binding: any) => void;
  unbind?: (el: HTMLElement) => void;
}

interface Observable<T> {
  value: T;
  subscribe(callback: (value: T) => void): () => void;
}

interface ComputedRef<T> {
  readonly value: T;
}

// 🏗️ Framework implementation
const createFramework = (): Framework => {
  const plugins = new Map<string, Plugin<any>>();
  const components = new Map<string, ComponentDefinition>();
  const directives = new Map<string, DirectiveDefinition>();
  
  let globalConfig: GlobalConfig = {
    debug: false,
    performance: false,
    devtools: true,
    silent: false
  };
  
  // Create the framework hybrid type
  const FrameworkConstructor = function(
    this: any,
    config: AppConfig
  ): App {
    const isConstructor = this instanceof FrameworkConstructor;
    
    // Create app instance
    const app: App = {
      $el: null!,
      $data: config.data || {},
      $mount(el?: string | HTMLElement) {
        const element = el || config.el;
        this.$el = typeof element === 'string' 
          ? document.querySelector(element)! 
          : element;
        
        // Mount logic here...
        config.mounted?.();
        
        if (globalConfig.debug) {
          console.log('App mounted:', this.$el);
        }
      },
      $destroy() {
        config.destroyed?.();
        // Cleanup logic here...
      },
      $on(event: string, handler: Function) {
        // Event system implementation
      },
      $off(event: string, handler?: Function) {
        // Event system implementation
      },
      $emit(event: string, ...args: any[]) {
        // Event system implementation
      }
    };
    
    if (!isConstructor) {
      app.$mount();
    }
    
    return app;
  } as any as Framework;
  
  // Add metadata
  FrameworkConstructor.version = '3.0.0';
  FrameworkConstructor.name = 'MiniFramework';
  
  // Global configuration
  FrameworkConstructor.config = globalConfig;
  FrameworkConstructor.configure = (options: Partial<GlobalConfig>) => {
    Object.assign(globalConfig, options);
    FrameworkConstructor.config = globalConfig;
  };
  
  // Plugin system
  FrameworkConstructor.use = function<T>(plugin: Plugin<T>, options?: T): Framework {
    plugins.set(plugin.name, plugin);
    // Initialize plugin
    return this;
  };
  FrameworkConstructor.plugins = plugins;
  
  // Component registry
  FrameworkConstructor.component = function(
    name: string, 
    definition: ComponentDefinition
  ): Framework {
    components.set(name, definition);
    return this;
  };
  FrameworkConstructor.components = components;
  
  // Directive registry
  FrameworkConstructor.directive = function(
    name: string,
    definition: DirectiveDefinition
  ): Framework {
    directives.set(name, definition);
    return this;
  };
  FrameworkConstructor.directives = directives;
  
  // Global API
  FrameworkConstructor.nextTick = (callback: () => void): Promise<void> => {
    return Promise.resolve().then(callback);
  };
  
  FrameworkConstructor.observable = <T>(value: T): Observable<T> => {
    const subscribers = new Set<(value: T) => void>();
    return {
      value,
      subscribe(callback: (value: T) => void) {
        subscribers.add(callback);
        return () => subscribers.delete(callback);
      }
    };
  };
  
  FrameworkConstructor.computed = <T>(getter: () => T): ComputedRef<T> => {
    return {
      get value() {
        return getter();
      }
    };
  };
  
  return FrameworkConstructor;
};

// 💫 Usage
const Framework = createFramework();

// Configure globally
Framework.configure({ debug: true });

// Register component
Framework.component('my-button', {
  template: '<button @click="onClick"><slot></slot></button>',
  props: ['variant'],
  methods: {
    onClick() {
      this.$emit('click');
    }
  }
});

// Register directive
Framework.directive('focus', {
  bind(el) {
    el.focus();
  }
});

// Create app - functional style
const app1 = Framework({
  el: '#app',
  data: {
    message: 'Hello World'
  },
  mounted() {
    console.log('App is ready!');
  }
});

// Create app - constructor style
const app2 = new Framework({
  el: '#app2',
  data: {
    count: 0
  },
  methods: {
    increment() {
      this.count++;
    }
  }
});

// Use global API
Framework.nextTick(() => {
  console.log('Next tick!');
});

const observable = Framework.observable(42);
observable.subscribe(val => console.log('Value changed:', val));

🛡️ Type Safety with Hybrid Types

🔍 Type Guards for Hybrid Types

Ensuring type safety with complex hybrids:

// 🎯 Type guard utilities
const isCallable = <T extends Function>(
  value: any
): value is T => {
  return typeof value === 'function';
};

const isConstructable = <T>(
  value: any
): value is new (...args: any[]) => T => {
  return typeof value === 'function' && value.prototype;
};

const hasProperty = <T, K extends keyof T>(
  obj: any,
  key: K
): obj is T => {
  return key in obj;
};

// 🏗️ Safe hybrid type usage
interface HybridAPI {
  // Callable
  (input: string): Result;
  
  // Constructor
  new (config: Config): Instance;
  
  // Static methods
  parse(data: string): ParsedData;
  stringify(data: ParsedData): string;
  
  // Properties
  version: string;
  plugins: Plugin[];
}

interface Result {
  success: boolean;
  data?: any;
}

interface Config {
  mode: 'development' | 'production';
  strict: boolean;
}

interface Instance {
  process(input: string): Result;
}

interface ParsedData {
  type: string;
  content: any;
}

// Type-safe factory
const createHybridAPI = (): HybridAPI => {
  const api = function(this: any, inputOrConfig: string | Config) {
    if (this instanceof api) {
      // Constructor call
      const config = inputOrConfig as Config;
      return {
        process(input: string): Result {
          // Implementation based on config
          return { success: true, data: input };
        }
      };
    } else {
      // Function call
      const input = inputOrConfig as string;
      return { success: true, data: input };
    }
  } as any as HybridAPI;
  
  // Add static methods
  api.parse = (data: string): ParsedData => {
    return { type: 'text', content: data };
  };
  
  api.stringify = (data: ParsedData): string => {
    return JSON.stringify(data);
  };
  
  // Add properties
  api.version = '1.0.0';
  api.plugins = [];
  
  return api;
};

// 💫 Type-safe usage
const api = createHybridAPI();

// Type guards in action
if (isCallable<(input: string) => Result>(api)) {
  const result = api('test'); // ✅ Type-safe function call
}

if (isConstructable<Instance>(api)) {
  const instance = new api({ mode: 'production', strict: true }); // ✅ Type-safe construction
}

if (hasProperty<HybridAPI, 'parse'>(api, 'parse')) {
  const parsed = api.parse('data'); // ✅ Type-safe static method
}

🎪 Common Pitfalls and Solutions

❌ Common Mistakes

// ❌ BAD: Losing type information
const badHybrid = (() => {
  const fn = () => 'result';
  fn.prop = 'value';
  return fn; // Type is just () => string
});

// ✅ GOOD: Preserving type information
interface GoodHybrid {
  (): string;
  prop: string;
}

const goodHybrid = (() => {
  const fn = (() => 'result') as GoodHybrid;
  fn.prop = 'value';
  return fn;
})();

// ❌ BAD: Forgetting to handle both call patterns
const badDualUse = function(this: any, arg: any) {
  // Only handles function call
  return processArg(arg);
};

// ✅ GOOD: Handling both patterns
const goodDualUse = function(this: any, arg: any) {
  if (this instanceof goodDualUse) {
    // Constructor call
    return new Instance(arg);
  } else {
    // Function call
    return processArg(arg);
  }
};

// ❌ BAD: Mutating shared state
const badCounter = (() => {
  let count = 0; // Shared between all uses
  const fn = () => count++;
  fn.reset = () => { count = 0; };
  return fn;
})();

// ✅ GOOD: Isolated state per instance
const createGoodCounter = () => {
  let count = 0; // Isolated per instance
  const fn = () => count++;
  fn.reset = () => { count = 0; };
  return fn;
};

🚀 Best Practices

📋 Design Guidelines

  1. Clear Intent 🎯
// Make the hybrid nature obvious
interface Calculator {
  // Clearly document each behavior
  (expression: string): number; // Evaluate expression
  new (precision: number): CalculatorInstance; // Create instance
  constants: { PI: number; E: number }; // Static constants
}
  1. Consistent Behavior 🔄
// Both call patterns should be related
const DateTime = function(this: any, input?: string | Date) {
  const date = input ? new Date(input) : new Date();
  
  if (this instanceof DateTime) {
    // Constructor: returns instance
    this.date = date;
    return this;
  } else {
    // Function: returns formatted string
    return date.toISOString();
  }
};
  1. Type Safety First 🛡️
// Use strict typing for all behaviors
interface StrictHybrid<T, R> {
  (input: T): R;
  new (config: T): { process(input: T): R };
  defaults: T;
  validate(input: unknown): input is T;
}

🎮 Hands-On Exercise

Let’s build a complete event emitter library with hybrid types!

📝 Challenge: Advanced Event System

Create an event system that:

  1. Can be called to emit events
  2. Can be instantiated for isolated event scopes
  3. Has static methods for global events
  4. Supports typed events
// Your challenge: Implement this interface
interface EventSystem<T extends Record<string, any[]> = any> {
  // Callable - emit global event
  <K extends keyof T>(event: K, ...args: T[K]): void;
  
  // Constructor - create scoped emitter
  new (): EventEmitter<T>;
  
  // Static methods
  on<K extends keyof T>(event: K, handler: (...args: T[K]) => void): void;
  off<K extends keyof T>(event: K, handler?: (...args: T[K]) => void): void;
  once<K extends keyof T>(event: K, handler: (...args: T[K]) => void): void;
  
  // Properties
  handlers: Map<keyof T, Set<Function>>;
  history: Array<{ event: keyof T; args: any[]; timestamp: number }>;
  
  // Utilities
  clear(): void;
  replay<K extends keyof T>(event: K): void;
}

interface EventEmitter<T extends Record<string, any[]>> {
  on<K extends keyof T>(event: K, handler: (...args: T[K]) => void): this;
  off<K extends keyof T>(event: K, handler?: (...args: T[K]) => void): this;
  emit<K extends keyof T>(event: K, ...args: T[K]): this;
  once<K extends keyof T>(event: K, handler: (...args: T[K]) => void): this;
}

// Define your event types
interface AppEvents {
  login: [user: { id: string; name: string }];
  logout: [];
  message: [text: string, sender: string];
  error: [error: Error];
}

// Implement the event system!

💡 Solution

Click to see the solution
const createEventSystem = <T extends Record<string, any[]>>(): EventSystem<T> => {
  // Global state
  const globalHandlers = new Map<keyof T, Set<Function>>();
  const history: Array<{ event: keyof T; args: any[]; timestamp: number }> = [];
  
  // Event emitter class
  class EventEmitterImpl implements EventEmitter<T> {
    private handlers = new Map<keyof T, Set<Function>>();
    
    on<K extends keyof T>(event: K, handler: (...args: T[K]) => void): this {
      if (!this.handlers.has(event)) {
        this.handlers.set(event, new Set());
      }
      this.handlers.get(event)!.add(handler);
      return this;
    }
    
    off<K extends keyof T>(event: K, handler?: (...args: T[K]) => void): this {
      if (handler) {
        this.handlers.get(event)?.delete(handler);
      } else {
        this.handlers.delete(event);
      }
      return this;
    }
    
    emit<K extends keyof T>(event: K, ...args: T[K]): this {
      this.handlers.get(event)?.forEach(handler => {
        handler(...args);
      });
      return this;
    }
    
    once<K extends keyof T>(event: K, handler: (...args: T[K]) => void): this {
      const wrappedHandler = (...args: T[K]) => {
        handler(...args);
        this.off(event, wrappedHandler as any);
      };
      return this.on(event, wrappedHandler as any);
    }
  }
  
  // Create the hybrid
  const EventSystemImpl = function<K extends keyof T>(
    this: any,
    event?: K,
    ...args: T[K]
  ) {
    if (this instanceof EventSystemImpl) {
      // Constructor call
      return new EventEmitterImpl();
    } else if (event) {
      // Function call - emit global event
      globalHandlers.get(event)?.forEach(handler => {
        (handler as Function)(...args);
      });
      
      // Record in history
      history.push({
        event,
        args,
        timestamp: Date.now()
      });
    }
  } as any as EventSystem<T>;
  
  // Static methods
  EventSystemImpl.on = <K extends keyof T>(
    event: K,
    handler: (...args: T[K]) => void
  ) => {
    if (!globalHandlers.has(event)) {
      globalHandlers.set(event, new Set());
    }
    globalHandlers.get(event)!.add(handler);
  };
  
  EventSystemImpl.off = <K extends keyof T>(
    event: K,
    handler?: (...args: T[K]) => void
  ) => {
    if (handler) {
      globalHandlers.get(event)?.delete(handler);
    } else {
      globalHandlers.delete(event);
    }
  };
  
  EventSystemImpl.once = <K extends keyof T>(
    event: K,
    handler: (...args: T[K]) => void
  ) => {
    const wrappedHandler = (...args: T[K]) => {
      handler(...args);
      EventSystemImpl.off(event, wrappedHandler as any);
    };
    EventSystemImpl.on(event, wrappedHandler as any);
  };
  
  // Properties
  EventSystemImpl.handlers = globalHandlers;
  EventSystemImpl.history = history;
  
  // Utilities
  EventSystemImpl.clear = () => {
    globalHandlers.clear();
    history.length = 0;
  };
  
  EventSystemImpl.replay = <K extends keyof T>(event: K) => {
    history
      .filter(entry => entry.event === event)
      .forEach(entry => {
        EventSystemImpl(entry.event, ...entry.args as any);
      });
  };
  
  return EventSystemImpl;
};

// 💫 Usage
const Events = createEventSystem<AppEvents>();

// Global events
Events.on('login', (user) => {
  console.log(`User ${user.name} logged in`);
});

Events.on('error', (error) => {
  console.error('Global error:', error);
});

// Emit global event
Events('login', { id: '123', name: 'Alice' });
Events('message', 'Hello!', 'Bob');

// Scoped emitter
const emitter = new Events();
emitter
  .on('message', (text, sender) => {
    console.log(`${sender}: ${text}`);
  })
  .emit('message', 'Hi there!', 'Charlie');

// Check history
console.log('Event history:', Events.history);

// Replay login events
Events.replay('login');

🎯 Summary

You’ve mastered hybrid types in TypeScript! 🎉 You learned how to:

  • 🔧 Create objects that combine multiple type behaviors
  • 🎭 Build jQuery-style APIs with hybrid patterns
  • 🔌 Design powerful plugin systems
  • 🏗️ Implement framework cores with hybrid types
  • 🛡️ Ensure type safety with complex hybrids
  • ✨ Apply best practices for maintainable hybrid APIs

Hybrid types showcase TypeScript’s incredible flexibility in modeling JavaScript’s dynamic nature while maintaining type safety. They’re the secret sauce behind many popular libraries!