Core JavaScript · 3 of 9
static members have been used for singleton state or accessing variables without an instance:
class FeatureFlag {
private static enabled = false;
static enable(): void {
this.enabled = true;
}
static isEnabled(): boolean {
return this.enabled;
}
}
FeatureFlag.enable();
FeatureFlag.isEnabled(); // true
A module's top-level code runs once, the first time it's imported, and every importer shares the same module. There's no need for a class or the static keyword to get "one shared instance" — that's just what a module is.
// feature-flag.js
let enabled = false;
export const enable = () => {
enabled = true;
};
export const isEnabled = () => {
return enabled;
};
// usage — every importer shares the same `enabled` flag
import { enable, isEnabled } from "./feature-flag.js";
enable();
isEnabled(); // true
Here enabled is also private by scope, the same way state was private-by-closure in the previous lesson — there's no equivalent of a public static field unless you deliberately export it. If you did want enabled itself readable from outside, you'd export a function that returns it, rather than exporting the mutable let binding directly.
The singleton pattern — a private constructor plus a static getInstance() — exists to guarantee exactly one instance no matter how many places ask for it:
class ConfigService {
private static instance: ConfigService;
private constructor(private readonly settings: Settings) {}
static getInstance(): ConfigService {
if (!ConfigService.instance) {
ConfigService.instance = new ConfigService(loadSettings());
}
return ConfigService.instance;
}
get(key: string) {
return this.settings[key];
}
}
ConfigService.getInstance().get("apiUrl");
A module already gives you single-instance behavior - a module is fetched, parsed, and evaluated once only. Every import of "./config-service.js" anywhere in the app resolves to the same module.
// config-service.js
const defaults = {
apiUrl: "https://api.example.com",
timeout: 5000,
};
let config = { ...defaults };
export const initialise = (state = {}) => {
config.timeout = state?.timeout || defaults.timeout;
config.apiUrl = state?.apiUrl || defaults.apiUrl;
};
export const getTimeout = () => {
return config.timeout;
};
// usage — call once at startup; every importer shares the same `config`
import { initialise, get } from "./config-service.js";
initialise({ timeout: 10000 });
get("apiUrl"); // "https://api.example.com" — untouched keys keep their default
initialise(); // no argument — every key falls back to the defaults
Passing state in keeps the module from reaching out for it at import time: a test can call initialise({ timeout: 0 }) without whatever loadSettings() would have touched. Callers pass only the keys they're changing.
The private constructor, the instance-guard if, and getInstance() all existed to compensate for classes not having module-level scope of their own. Once the module is the scope, that plumbing has nothing left to do.