Modules Over Singletons in TypeScript
Published on Aug 15, 2023
Modules Over Singletons in TypeScript: A Pragmatic Approach
In software development, certain challenges present themselves repeatedly. This repetition has paved the way for design patterns, serving as blueprints for common problems. The Singleton pattern is one such blueprint, ensuring only one instance of a class exists. But when we shift our gaze to TypeScript, a different player enters the arena: Modules. With an inherent feature set that provides much of what Singletons aim to achieve, Modules emerge as a more idiomatic choice for TypeScript developers. Here’s why.
The Nature of TypeScript
TypeScript extends JavaScript, introducing static typing, interfaces, and a robust module system. This module system ensures that when a module is imported, the same instance is consistently returned. It’s essentially a built-in Singleton mechanism without the need for the traditional Singleton structure.
Simplicity with Modules
Modules in TypeScript are straightforward. They encapsulate functionality, expose what’s needed, and keep everything else private. Consider this configuration:
// configModule.ts
export const apiUrl = "https://api.example.com";
export const apiKey = "12345";
Each import of configModule
retrieves the same data. No additional patterns or structures are necessary.
Flexibility and Change
Starting with a module and then realizing you need multiple instances later is far simpler with TypeScript modules than with traditional Singletons. Export the class or function, create instances as required, and adapt without restructuring your entire pattern.
Less Boilerplate
Singletons require structure: private constructors, static methods, etc. Modules in TypeScript are more concise, cutting down on unnecessary boilerplate.
Example: Simple Singleton to Module
Singleton Approach:
class SingletonConfig {
private static instance: SingletonConfig;
public apiUrl: string;
private constructor() {
this.apiUrl = "https://api.example.com";
}
public static getInstance(): SingletonConfig {
if (!SingletonConfig.instance) {
SingletonConfig.instance = new SingletonConfig();
}
return SingletonConfig.instance;
}
}
const configInstance = SingletonConfig.getInstance();
Module Approach:
// configModule.ts
export const apiUrl = "https://api.example.com";
Example: Complex Singleton to Module
Singleton Approach:
class SingletonDatabase {
private static instance: SingletonDatabase;
private connection: any;
private constructor() {
this.connection = {}; // mock connection logic
}
public static getInstance(): SingletonDatabase {
if (!SingletonDatabase.instance) {
SingletonDatabase.instance = new SingletonDatabase();
}
return SingletonDatabase.instance;
}
query(sql: string) {
// query logic using this.connection
}
}
const dbInstance = SingletonDatabase.getInstance();
Module Approach:
// databaseModule.ts
const connection = {}; // mock connection logic
export function query(sql: string) {
// query logic using connection
}
Conclusion
While Singletons have their place, TypeScript’s architecture nudges developers toward using modules. When developing in TypeScript, modules provide a clean, consistent, and flexible approach that aligns more naturally with the language’s design philosophy.