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:
- jQuery 💰:
$()
is a function, but$.ajax
is a method - Express 🚂:
app()
creates middleware, butapp.get()
defines routes - Lodash 🔧:
_()
wraps values, but_.map
is a utility - Moment.js ⏰:
moment()
creates dates, butmoment.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
- 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
}
- 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();
}
};
- 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:
- Can be called to emit events
- Can be instantiated for isolated event scopes
- Has static methods for global events
- 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!