Stage: 0 – Strawperson (draft for discussion)
Champions: Seeking champions
Authors: Your Name Here (@handle)
Repo: to be created
This proposal introduces init-only classes in JavaScript. These are classes that:
- Declare an instance method named
initializer(...args)instead of a constructor. - Cannot define both
initializerandconstructor(syntax error if both appear). - Can only be instantiated using a new
init ClassName(args)expression. - Throw hard errors if someone tries to use
newon an init-only class oriniton a normal class.
Init-only classes support:
- Lazy, parameterized initialization.
- Async initialization with built-in concurrency control.
- Configurable re-initialization policies.
- Per-realm semantics with predictable behavior.
Many applications need one-time or service-style objects (e.g., configuration roots, caches, app singletons) that are not created repeatedly. Today developers emulate this with:
- ES modules (which are one-time but eager and parameterless),
- Factories (ad hoc, no standard errors or concurrency semantics),
- Hand-written singletons (boilerplate heavy, error-prone),
- Dependency injection frameworks (non-standard, often overkill).
init and init-only classes offer:
- Clarity: no accidental
newwhere only one instance should exist. - Safety: explicit errors for misuse.
- Async correctness: concurrent initialization calls coalesce automatically.
- Interoperability: works with private fields, decorators, modules, bundlers, and TypeScript.
- A concise, declarative way to define classes that must be initialized, not constructed repeatedly.
- Familiar class syntax with minimal new concepts.
- Clear errors for misuse (
newon init-only,initon normal). - Lazy instantiation with optional async initialization.
- Configurable reinitialization policies (default, strict, or reinit).
- Per-realm semantics with no global leakage.
- Replacing ES modules’ existing one-time evaluation model.
- Providing a full lifecycle model beyond initialization (e.g., disposal).
- Supporting arbitrary global singletons shared across realms.
- Kotlin:
objectkeyword provides singleton objects. - Swift:
static let shared = …idiom for global singletons. - C#:
Lazy<T>for deferred initialization. - JavaScript: ES modules as one-time, parameterless singletons; factories and service patterns in frameworks like Angular.
These patterns inspired the need for standardized language support with clear semantics.
class App {
initializer({ port }) { this.port = port; }
start() { console.log(`App on ${this.port}`); }
}- Declaring an
initializermethod marks the class as init-only. constructorandinitializercannot coexist.- Engine enforces init-only semantics automatically.
const app = init App({ port: 3000 });- First call allocates the instance and calls
initializer(...args). - Later calls return the same instance (policy configurable).
new App()→ TypeError: Useinit App(...)instead.init User()on a normal class → TypeError: Usenew User(...)instead.
class Config {
static [Symbol.initPolicy] = 'strict'; // 'return-first' | 'strict' | 'reinit'
initializer(opts) { this.opts = opts; }
}return-first(default): return the first instance, ignore later args.strict: throw on any subsequentinitcall.reinit: allow explicitreset()to replace instance (optional).
class DB {
async initializer(url) {
this.conn = await connect(url);
}
}
const db = await init DB('postgres://…');- Concurrent
initcalls share the same in-flight promise. - After resolution, subsequent
initcalls return the instance synchronously.
App.instance // returns the instance or throws if uninitialized- Class with
initializer→ init-only. - Syntax error if both
initializerandconstructorexist. - Engine synthesizes a constructor automatically; user cannot override it.
-
init C(...args):- If memoized instance exists → return or throw per policy.
- Else allocate instance, call
initializer, memoize result. - If
initializerreturns a promise, memoize in-flight promise until it resolves.
-
new C(...)on init-only → TypeError. -
init C(...)on normal → TypeError.
- Memoization is per realm; workers/iframes have separate instances.
- Init-only status does not propagate; subclasses must declare
initializerexplicitly. Symbol.superArgsmay customize argument forwarding tosuper().
- Unchanged; init-only semantics orthogonal to field initialization and decorators.
| Feature | ES Modules | Factories | Singleton Class (manual) | Init-only Classes |
|---|---|---|---|---|
| Lazy instantiation | ❌ | ✅ | ✅ | ✅ |
| Parameterized initialization | ❌ | ✅ | ✅ | ✅ |
| Enforced single instance | ❌ | ❌ | ❌ | ✅ |
| Built-in async coalescing | ❌ | ❌ | ❌ | ✅ |
| Standardized error handling | ❌ | ❌ | ❌ | ✅ |
| Works across realms | ✅ | ✅ | ✅ | ✅ |
class App {
initializer({ port }) { this.port = port; }
}
const a1 = init App({ port: 3000 });
const a2 = init App({ port: 4000 }); // returns same instanceclass Settings {
static [Symbol.initPolicy] = 'strict';
initializer(opts) { this.opts = opts; }
}
init Settings({ env: 'prod' });
init Settings({ env: 'dev' }); // throwsclass DB {
async initializer(url) {
this.conn = await connect(url);
}
}
await init DB('postgres://…');class Service { initializer() {} }
new Service(); // TypeError: Use init Service(...)
class User {}
init User(); // TypeError: Use new User(...)- Modules are one-time, parameterless, eager.
- Init-only classes provide lazy, parameterized, async-safe initialization.
declare function init<C extends abstract new (...a: any) => any>(
ctor: C,
...args: ConstructorParameters<C>
): InstanceType<C> | Promise<InstanceType<C>>;- Lint rules can forbid
newon classes withinitializer.
- Mark classes with
initializeras init-only via metadata. - Provide
init()helper that enforces rules and memoization. - Proxy constructors to throw on
newif init-only.
- Should
Symbol.superArgsdefault to forwarding all args or none? - Should
reinitpolicy be standardized now or later? - Should
initializersupport decorators for policy config?
- Instances live for the realm lifetime unless reset explicitly.
- Async initialization coalesces to prevent races.
- v0.1: Initial draft with
initializer,init, async support, policies, inheritance rules.
Any feedback appreciated!