Class Features — Private Fields, Static Blocks, Decorators
TL;DR
Modern class features added since ES2022: #private fields (real privacy enforced by the engine, not convention), static blocks (static { ... } for one-time init in a class body), and decorators (Stage 3, @decorator syntax for class/method/field meta-programming). All three are now in TC39 Stage 4 or near-final. The big mental shift: #x is real privacy — not the TypeScript private keyword, which only prevents TS errors and is ignored at runtime.
Interview Q&A
Q: #private vs TypeScript private — what’s the difference?
A:
TS private |
#private |
|
|---|---|---|
| Layer | type-time only | runtime |
| At runtime | accessible (obj["private"] works in JS) |
not accessible (throws / undefined outside class) |
| In compiled JS | gone (just a normal property) | #name becomes a WeakMap entry, truly inaccessible |
| Reflect.has | finds it | does not find #name |
| Subclass access | yes (in TS) | no — #x is class-only |
class Cat {
private name = "Felix"; // TS-private
#age = 3; // truly private
}
const c = new Cat();
// In .ts: c.name → TS error, but at runtime it works (c["name"] returns "Felix")
// c.#age → SyntaxError at parse: "Private field '#age' must be declared in an enclosing class"
For real privacy (other JS code can’t access), use #. TS private is a hint; JS engines don’t enforce it.
Q: Show me #private features.
A:
class BankAccount {
#balance: number;
static #nextId = 1; // private static
#id: number;
constructor(balance: number) {
this.#balance = balance;
this.#id = BankAccount.#nextId++;
}
deposit(n: number) { this.#balance += n; }
get balance() { return this.#balance; }
// Private method
#computeInterest() { return this.#balance * 0.05; }
applyInterest() { this.#balance += this.#computeInterest(); }
}
All # members: private fields, private methods, private static fields, private static methods, private getters/setters. Real privacy.
Q: #in check — does this object have the private field?
A:
class Animal {
#name = "x";
static isAnimal(obj: unknown): obj is Animal {
return typeof obj === "object" && obj !== null && #name in obj;
}
}
console.log(Animal.isAnimal(new Animal())); // true
console.log(Animal.isAnimal({})); // false
#x in obj is the “brand check” — confirms an object is a real instance of the class (not a duck-typed lookalike). Useful for runtime type discrimination.
Q: Static initialization blocks — what’s the use?
A: Initialize static state that needs control flow (try/catch, loops, async-light setup):
class Config {
static #data: Record<string, unknown>;
static {
try {
Config.#data = JSON.parse(globalThis.__INITIAL_CONFIG__ ?? "{}");
} catch (e) {
console.error("bad config", e);
Config.#data = {};
}
if (!Config.#data.timeout) Config.#data.timeout = 30_000;
}
static get(key: string) { return Config.#data[key]; }
}
Runs once when the class is defined. Cleaner than module-level IIFE for class-scoped init.
Multiple static {} blocks are allowed; they run in source order.
Q: Decorators — what stage are we at?
A: Stage 3 at TC39; specced, implemented by TypeScript 5.0+, supported by SWC and Babel, available in modern Node and bundlers. Different from the legacy TS experimental decorators (experimentalDecorators: true) — the new spec is its own thing.
Q: Decorator syntax — show me.
A:
function logged<T extends new (...args: any[]) => any>(target: T, ctx: ClassDecoratorContext) {
ctx.addInitializer(function () {
console.log(`Created ${ctx.name}`);
});
return target;
}
function bound(target: Function, ctx: ClassMethodDecoratorContext) {
return function (this: any, ...args: any[]) {
return target.apply(this, args);
};
}
@logged
class Widget {
@bound
handleClick() { console.log("clicked"); }
}
Decorators receive:
- The target (class, method, field, accessor, getter, setter).
- A context object with
kind,name,addInitializer,privateflag, etc.
Use cases:
- Method binding (
@boundto auto-bindthis). - Logging / instrumentation (
@deprecated,@logged). - Memoization (
@memoized). - Framework integration (Angular components, MobX
@observable, NestJS controllers).
Q: Are decorators worth using now?
A: Yes for new code in supportive ecosystems (Angular, NestJS, MobX, LitElement). Cautious for general TS apps — debugging and tooling around decorators is still maturing.
The legacy experimental decorators (still common in Angular, NestJS) are different syntax + semantics. Migration to stage-3 decorators is in progress for many frameworks.
If you’re writing a library that exposes decorators: use stage-3 syntax (tsconfig without experimentalDecorators). If you’re maintaining legacy code with @injectable, stay on the old spec until the framework upgrades.
Q: Auto-accessors — what?
A: A stage-3 sibling of decorators: declare a field as auto-accessor (accessor keyword) so decorators can intercept reads/writes:
function debounced<T>(ms: number) {
return function (target: ClassAccessorDecoratorTarget<unknown, T>, ctx: ClassAccessorDecoratorContext) {
let timer: any;
return {
get() { return target.get.call(this); },
set(value: T) {
clearTimeout(timer);
timer = setTimeout(() => target.set.call(this, value), ms);
},
};
};
}
class Search {
@debounced(300) accessor query = "";
}
accessor query = "" desugars to a private field + get/set. Useful for reactive frameworks adopting decorators.
Q: Why use classes at all in modern React/Vue?
A: Increasingly rare. React abandoned class components for hooks; Vue’s Options API still uses class-like structure but moved to Composition API. Classes still fit for:
- Domain models (
User,Order,Cart) — clearer than nested objects for behavior + state. - Service classes (
ApiClient,EventBus,Logger) — singleton-style. - Error subclasses —
class NetworkError extends Error { constructor(...) { ... } }. - Framework integration that requires classes (NestJS, Angular).
- DOM custom elements / web components (
class MyElement extends HTMLElement).
For UI components in React: hooks-functional all the way. Vue’s Composition API + <script setup> is also class-less.
Gotchas / edge cases
#xoutside the declaring class is a parse error, not a runtime one — can’t be conditionally accessed.#x in objis the only safe brand check —instanceofcan lie across realms (iframes, workers).- Subclass access —
#xis class-private, not protected. A subclass cannot access parent’s#x. Use TSprotected(TS-only) or design via methods. - Decorators can’t change the kind of thing — a class decorator returns a class; a method decorator returns a method. The runtime guards against returning the wrong shape.
- Decorator evaluation order — class decorators run after method decorators; bottom-up within each kind.
static {}andawait— top-levelawaitdoesn’t work inside a static block. Use an async IIFE if needed, or set up the static differently.- Compile target matters —
target: "ES2022"for native#x; older targets transpile toWeakMap(still private but with overhead).
What a senior is expected to say
- “
#xis engine-enforced privacy; TSprivateis type-time only. Use#when other JS code must not access — like security-relevant state.” - “
#x in objis the brand check pattern — confirms an instance even across realms or inside duck-typed mocks.” - “Static blocks for one-time class-scoped init with control flow. Cleaner than module-level IIFE for class-bound state.”
- “Stage 3 decorators are real now — TS 5.0+, modern bundlers. Different syntax than legacy
experimentalDecorators; migrate when frameworks do.” - “Most React/Vue UI doesn’t use classes. Classes for domain models, services, error subclasses, web components, NestJS/Angular.”
Cross-references
- TypeScript class typing: ../04_typescript/
- React function components vs classes: ../05_react/
- Iterators/generators (often paired with class implementations): 04_iterators_and_generators.md
Further reading
- MDN — Private class features: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties
- MDN —
staticinitialization blocks: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Static_initialization_blocks - TC39 — Decorators: https://github.com/tc39/proposal-decorators
- TypeScript 5.0 — Decorators: https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#decorators