4.3 Static Members and Advanced Class Patterns
Static members are properties and methods that belong to the class itself, not to any instance. You call them directly with ClassName.method() without creating an object via new. They are used for things that "need to exist at the class level, not the instance level" — utility functions, factory methods, and singleton state management.
This chapter covers practical patterns: from the basics of static members, through ES2022 static initialization blocks, Singleton and Factory patterns, the this type for Fluent Interfaces, and getters/setters.
static Properties and Methods
Adding the static keyword makes a member class-level. It cannot be accessed from an instance — only through the class name.
class MathUtils {
static readonly PI: number = 3.14159265358979;
static circleArea(radius: number): number {
return MathUtils.PI * radius ** 2;
}
static degreesToRadians(degrees: number): number {
return degrees * (MathUtils.PI / 180);
}
static clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
}
console.log(MathUtils.circleArea(5)); // 78.539...
console.log(MathUtils.degreesToRadians(180)); // 3.14159...
console.log(MathUtils.clamp(150, 0, 100)); // 100
// const m = new MathUtils();
// m.circleArea(5); // Error: static members are not accessible on instances
Static Counter Example
Use a static property to manage shared state across all instances.
class UserSession {
private static activeCount: number = 0;
private static sessions: Map<string, UserSession> = new Map();
readonly sessionId: string;
readonly userId: number;
private startTime: Date;
constructor(userId: number) {
this.userId = userId;
this.sessionId = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
this.startTime = new Date();
UserSession.activeCount++;
UserSession.sessions.set(this.sessionId, this);
}
end(): void {
UserSession.sessions.delete(this.sessionId);
UserSession.activeCount--;
console.log(`Session ${this.sessionId} ended. Active sessions: ${UserSession.activeCount}`);
}
static getActiveCount(): number {
return UserSession.activeCount;
}
static findByUserId(userId: number): UserSession | undefined {
for (const session of UserSession.sessions.values()) {
if (session.userId === userId) return session;
}
return undefined;
}
}
const s1 = new UserSession(1001);
const s2 = new UserSession(1002);
console.log(UserSession.getActiveCount()); // 2
s1.end();
console.log(UserSession.getActiveCount()); // 1
Static Initialization Block (ES2022)
Complex static initialization logic goes inside a static { } block. It runs once when the class is first loaded. TypeScript 4.4+ supports this feature.
class DatabaseConfig {
static readonly host: string;
static readonly port: number;
static readonly dbName: string;
static readonly connectionString: string;
static readonly isProduction: boolean;
static {
// Complex initialization logic: try/catch and branching are all fine here
DatabaseConfig.isProduction = process.env.NODE_ENV === "production";
if (DatabaseConfig.isProduction) {
DatabaseConfig.host = process.env.DB_HOST ?? "prod-db.example.com";
DatabaseConfig.port = parseInt(process.env.DB_PORT ?? "5432", 10);
DatabaseConfig.dbName = process.env.DB_NAME ?? "prod_db";
} else {
DatabaseConfig.host = "localhost";
DatabaseConfig.port = 5432;
DatabaseConfig.dbName = "dev_db";
}
DatabaseConfig.connectionString =
`postgresql://${DatabaseConfig.host}:${DatabaseConfig.port}/${DatabaseConfig.dbName}`;
console.log(
`DB config initialized: ${DatabaseConfig.connectionString}`
);
}
}
// The static block runs the first time the class is accessed
console.log(DatabaseConfig.connectionString);
Multiple static blocks can be declared; they execute in declaration order.
class MultiStageInit {
static stage1: string;
static stage2: string;
static {
MultiStageInit.stage1 = "Stage 1 initialized";
console.log(MultiStageInit.stage1);
}
static {
MultiStageInit.stage2 = `${MultiStageInit.stage1} — Stage 2 ready`;
console.log(MultiStageInit.stage2);
}
}
Singleton Pattern
Use this when only one instance of a class should exist across the entire application. Implement it with a private constructor and a static getInstance() method.
class Logger {
private static instance: Logger | null = null;
private logs: string[] = [];
private logLevel: "debug" | "info" | "warn" | "error" = "info";
// private constructor: new Logger() is not callable from outside
private constructor() {
console.log("Logger instance created (first time only)");
}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
setLevel(level: "debug" | "info" | "warn" | "error"): void {
this.logLevel = level;
}
private shouldLog(level: "debug" | "info" | "warn" | "error"): boolean {
const levels = ["debug", "info", "warn", "error"];
return levels.indexOf(level) >= levels.indexOf(this.logLevel);
}
private log(level: "debug" | "info" | "warn" | "error", message: string): void {
if (!this.shouldLog(level)) return;
const entry = `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}`;
this.logs.push(entry);
console.log(entry);
}
debug(message: string): void { this.log("debug", message); }
info(message: string): void { this.log("info", message); }
warn(message: string): void { this.log("warn", message); }
error(message: string): void { this.log("error", message); }
getLogs(): string[] { return [...this.logs]; }
clearLogs(): void { this.logs = []; }
}
// Usage
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
console.log(logger1 === logger2); // true — same instance
logger1.info("Server started");
logger2.warn("Low memory warning"); // same instance as logger1
// new Logger(); // Error: private constructor
Factory Method Pattern
Instead of exposing the constructor directly, create objects through static factory methods. This is also known as the Named Constructor pattern.
class Color {
private constructor(
public readonly r: number,
public readonly g: number,
public readonly b: number,
public readonly a: number = 1
) {
if ([r, g, b].some((v) => v < 0 || v > 255)) {
throw new Error("RGB values must be in the range 0–255.");
}
if (a < 0 || a > 1) {
throw new Error("Alpha value must be in the range 0–1.");
}
}
// Factory methods — meaningful names for different construction approaches
static fromRGB(r: number, g: number, b: number): Color {
return new Color(r, g, b);
}
static fromHex(hex: string): Color {
const clean = hex.replace("#", "");
return new Color(
parseInt(clean.slice(0, 2), 16),
parseInt(clean.slice(2, 4), 16),
parseInt(clean.slice(4, 6), 16)
);
}
static fromRGBA(r: number, g: number, b: number, a: number): Color {
return new Color(r, g, b, a);
}
static white(): Color { return new Color(255, 255, 255); }
static black(): Color { return new Color(0, 0, 0); }
static transparent(): Color { return new Color(0, 0, 0, 0); }
toHex(): string {
const toHex = (n: number) => n.toString(16).padStart(2, "0");
return `#${toHex(this.r)}${toHex(this.g)}${toHex(this.b)}`;
}
toString(): string {
return this.a === 1
? `rgb(${this.r}, ${this.g}, ${this.b})`
: `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`;
}
withAlpha(a: number): Color {
return new Color(this.r, this.g, this.b, a);
}
}
const red = Color.fromRGB(255, 0, 0);
const blue = Color.fromHex("#0000FF");
const semiTransparent = Color.fromRGBA(128, 128, 128, 0.5);
console.log(red.toHex()); // #ff0000
console.log(blue.toString()); // rgb(0, 0, 255)
console.log(semiTransparent.toString()); // rgba(128, 128, 128, 0.5)
console.log(Color.white().toHex()); // #ffffff
this Type — Fluent Interface (Method Chaining)
Using this as the return type keeps chaining intact even in subclasses.
class QueryBuilder {
protected conditions: string[] = [];
protected selectedColumns: string[] = ["*"];
protected tableName: string = "";
protected limitValue: number | null = null;
protected offsetValue: number | null = null;
protected orderByClause: string | null = null;
from(table: string): this {
this.tableName = table;
return this;
}
select(...columns: string[]): this {
this.selectedColumns = columns;
return this;
}
where(condition: string): this {
this.conditions.push(condition);
return this;
}
limit(n: number): this {
this.limitValue = n;
return this;
}
offset(n: number): this {
this.offsetValue = n;
return this;
}
orderBy(column: string, direction: "ASC" | "DESC" = "ASC"): this {
this.orderByClause = `${column} ${direction}`;
return this;
}
build(): string {
const cols = this.selectedColumns.join(", ");
let query = `SELECT ${cols} FROM ${this.tableName}`;
if (this.conditions.length > 0) {
query += ` WHERE ${this.conditions.join(" AND ")}`;
}
if (this.orderByClause) {
query += ` ORDER BY ${this.orderByClause}`;
}
if (this.limitValue !== null) {
query += ` LIMIT ${this.limitValue}`;
}
if (this.offsetValue !== null) {
query += ` OFFSET ${this.offsetValue}`;
}
return query;
}
}
// The this type keeps chaining unbroken in the subclass
class PostgresQueryBuilder extends QueryBuilder {
withNoLock(): this {
// Add a PostgreSQL hint
return this;
}
returning(...columns: string[]): this {
// Add a RETURNING clause
return this;
}
}
const query = new PostgresQueryBuilder()
.from("users")
.select("id", "name", "email")
.where("age > 18")
.where("status = 'active'")
.orderBy("created_at", "DESC")
.limit(20)
.offset(40)
.withNoLock() // subclass method
.build(); // chain is unbroken
console.log(query);
// SELECT id, name, email FROM users
// WHERE age > 18 AND status = 'active'
// ORDER BY created_at DESC LIMIT 20 OFFSET 40
getter/setter — Computed Properties and Validation
The get and set keywords let you use property-like syntax while executing logic internally.
class Temperature {
private _celsius: number;
constructor(celsius: number) {
this._celsius = celsius;
}
// getter: computed property
get celsius(): number {
return this._celsius;
}
// setter: includes validation
set celsius(value: number) {
if (value < -273.15) {
throw new Error("Temperature cannot go below absolute zero (-273.15°C).");
}
this._celsius = value;
}
get fahrenheit(): number {
return this._celsius * (9 / 5) + 32;
}
set fahrenheit(value: number) {
this.celsius = (value - 32) * (5 / 9);
}
get kelvin(): number {
return this._celsius + 273.15;
}
toString(): string {
return `${this._celsius}°C / ${this.fahrenheit.toFixed(1)}°F / ${this.kelvin.toFixed(2)}K`;
}
}
const temp = new Temperature(100);
console.log(temp.toString()); // 100°C / 212.0°F / 373.15K
temp.fahrenheit = 32;
console.log(temp.celsius); // 0
console.log(temp.toString()); // 0°C / 32.0°F / 273.15K
// temp.celsius = -300; // Error: below absolute zero
Practical Example: Singleton Logger, Chaining Query Builder, and Config Manager
Config Manager (Singleton + getter/setter)
type LogLevel = "debug" | "info" | "warn" | "error";
type Environment = "development" | "staging" | "production";
interface AppSettings {
env: Environment;
logLevel: LogLevel;
maxRetries: number;
timeout: number;
apiBaseUrl: string;
}
class AppConfig {
private static instance: AppConfig;
private settings: AppSettings;
private constructor() {
// Default values
this.settings = {
env: (process.env.NODE_ENV as Environment) ?? "development",
logLevel: "info",
maxRetries: 3,
timeout: 5000,
apiBaseUrl: "https://api.example.com",
};
}
static getInstance(): AppConfig {
if (!AppConfig.instance) {
AppConfig.instance = new AppConfig();
}
return AppConfig.instance;
}
get env(): Environment { return this.settings.env; }
get logLevel(): LogLevel { return this.settings.logLevel; }
set logLevel(level: LogLevel) {
this.settings.logLevel = level;
console.log(`Log level changed to: ${level}`);
}
get maxRetries(): number { return this.settings.maxRetries; }
set maxRetries(value: number) {
if (value < 0 || value > 10) {
throw new Error("maxRetries must be between 0 and 10.");
}
this.settings.maxRetries = value;
}
get isProduction(): boolean {
return this.settings.env === "production";
}
get apiBaseUrl(): string { return this.settings.apiBaseUrl; }
// Fluent-style bulk update
configure(partial: Partial<AppSettings>): this {
Object.assign(this.settings, partial);
return this;
}
snapshot(): Readonly<AppSettings> {
return { ...this.settings };
}
}
// Usage
const config = AppConfig.getInstance();
config
.configure({ timeout: 10000, maxRetries: 5 });
config.logLevel = "debug";
console.log(config.snapshot());
console.log(config.isProduction); // false (development environment)
Pro Tips
static Members and Inheritance
Static members are accessible in subclasses. Using this inside a static method preserves the subclass context.
class Base {
static type: string = "Base";
static create(): Base {
console.log(`Creating: ${this.type}`);
return new this(); // new this() creates an instance of the current class
}
}
class Child extends Base {
static type: string = "Child";
}
const b = Base.create(); // Creating: Base
const c = Child.create(); // Creating: Child — subclass context is preserved
Static Factory vs Constructor
| Criterion | Constructor | Static Factory |
|---|---|---|
| Name | None (class name) | Can have a meaningful name |
| Caching | Not possible | Possible (singleton, cached return) |
| Return type | Always the class | Can return a subtype |
| Overloading | Limited | Fully flexible signatures |
| Failure handling | Exception only | Can return null / undefined / Result |
class Connection {
private static pool: Connection[] = [];
private constructor(private url: string) {}
// Return from cache or create a new connection
static getConnection(url: string): Connection {
const cached = Connection.pool.find((c) => c.url === url);
if (cached) {
console.log("Returning cached connection");
return cached;
}
const conn = new Connection(url);
Connection.pool.push(conn);
return conn;
}
// Return null on failure (impossible with a constructor)
static tryCreate(url: string): Connection | null {
try {
if (!url.startsWith("http")) return null;
return new Connection(url);
} catch {
return null;
}
}
}
Summary
| Concept | Syntax | Key Characteristic |
|---|---|---|
| Static property | static prop: T | Class-level state, shared across instances |
| Static method | static method() | Called without an instance |
| Static initialization block | static { } | Complex initialization, try/catch supported |
| Singleton | private constructor + static getInstance() | Guarantees a single instance |
| Factory method | static create() | Meaningful names, caching, flexible return |
| this type | Return type this | Preserves subclass chaining |
| getter | get prop() | Computed property, can be read-only |
| setter | set prop(v) | Validation and side effects |
In the next chapter we learn TypeScript decorators — a powerful metaprogramming tool for injecting metadata or transforming behavior on classes, methods, properties, and parameters. We will explore them alongside real-world NestJS and TypeORM examples.