Language Internals
Part 1 of 6 · JS/TS Language ProficiencyJavaScript & TypeScript — Intermediate to Advanced Proficiency
Hub: intermediate–advanced JS/TS language proficiency — event loop, closures/this/prototypes, memory/GC, TS types, modules/emit — not React/Next.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
What does one JavaScript realm execute at a time?
Answer
One call stack. Host APIs finish off that stack and enqueue callbacks. Workers are separate realms.
L2
Where do promise reactions sit relative to setTimeout?
Answer
Promise reactions are microtasks. After the current stack they drain before the next timer task.
L3
What does a closure actually capture?
Answer
The environment of the free variables the function references, not a snapshot of their values unless the binding itself is per iteration.
L4
Why does extracting a method often lose its receiver?
Answer
this is set by the call site. A bare call has no receiver. An arrow does not get a call-site this at all.
L5
When does nulling a variable fail to free an object?
Answer
When another root still reaches it: a listener, a Map, a timer, or a module singleton.
L6
Why can a value with an extra field still be passed as a Point?
Answer
TypeScript is structural. Excess property checks apply to fresh object literals, not to a value that already has a wider type.
L7
What breaks for consumers when emit settings are wrong?
Answer
module, moduleResolution, and declaration decide the public contract. A wrong exports.types path yields any or a broken go-to-definition even when the JS runs.
Failure modes
Microtask and macrotask mix-up
A then-callback and a zero-delay timer are treated as the same queue, so log order and races get explained backwards.
Lexical this for methods
A method is assumed to close over its object. Extracting it, or writing it as an arrow, drops or freezes the receiver.
Unintended roots
Detached DOM, listeners, and unbounded Maps keep graphs alive after the variable you nullled is gone.
Nominal reading of TypeScript
Two types with the same shape are treated as incompatible because their names differ, which is a Java habit, not the default checker.
Emit settings fought instead of aligned
module, moduleResolution, and declaration disagree with the runtime and the package exports map.
Misconceptions
Single-threaded JavaScript cannot have concurrency bugs.
One call stack per realm still interleaves tasks and microtasks. Races show up as ordering bugs, not as torn OS-thread writes.
async/await uses a different scheduler from promises.
await is syntax over a promise. Resuming after await is a microtask.
TypeScript types are still present when the program runs.
The checker erases types. What ships is JavaScript plus the declaration files you chose to emit.
Framework fluency covers this cluster.
Hooks and components sit on closures and the event loop. The language rules are the lesson. The framework API is a different curriculum.
Interviewer traps
Answering a microtask question with a hook diagram.
Name the call stack, the microtask queue, and one macrotask. Then stop. Framework scheduling is out of scope.
Saying await blocks the thread.
The async function returns a promise and resumes later. The thread keeps running other tasks.
Treating delete as an immediate free.
delete removes an own property. The value is collected only when it is unreachable and GC runs.
Design scenario
Same prompt for every reader.
Requirements
Explain the queue order, the closure that retains the buffer, and which TypeScript setting would have made the public helper's contract honest.
Traffic / scale
A few thousand in-flight requests, each awaiting two downstream calls.
Latency
A zero-delay timer should run once the stack and the microtask queue are clear, within a small multiple of one turn.
Consistency
A then-callback must observe state from the current turn, not from a later timer.
Availability
The process stays up. The failure mode is stalls and retained heap, not a crash.
Failure assumptions
- A promise reaction schedules another microtask on every turn.
- A listener or a Map still points at the buffer after the handler returns.
- Consumers import the helper from package exports, not from a deep source path.
Constraints
- Stay on language semantics. Do not redesign a React tree.
- Name the queue, the root, and the emit setting.
Prompt
A Node service logs a timer callback after a promise chain the author expected to run later, and a large buffer stays on the heap after the request handler returns.
What this cluster trains
Prefer
Language internals
Scheduling, scope, the object model, reachability, and the type checker. You can explain a log line and a declaration file without a framework diagram.
- Microtasks drain before the next timer task.
- this follows the call, except on arrows.
- Types describe JavaScript. Emit settings decide what consumers receive.
Alternative
Framework surface only
Hooks, components, and bundler presets. Useful later. They sit on the rules in this cluster and do not replace them.
- A stale closure in a hook is still a closure.
- A render scheduled after a promise is still a microtask story.
- A green build with skipLibCheck can still ship a lying .d.ts.
Read the cluster in this order
Each page is one mechanism. The hub is only the map.
- 1
Event loop
Call stack, microtasks, macrotasks, and what async/await desugars to. - 2
Closures, this, prototypes
Lexical environments, call-site this, and the live prototype chain behind class. - 3
Memory and GC
Reachability, WeakMap and WeakRef, and the roots that keep a graph alive. - 4
Type system
Generics, conditional types, mapped types, and inference. - 5
Modules and emit
ESM and CJS, tsconfig strictness, declaration files, and package exports.
Overview
This cluster is intermediate-to-advanced language proficiency for senior interviews and for production debugging. The goal is to explain why async behaves as it does, to find a retention leak, and to write types that encode a real invariant.
JavaScript in one realm runs a single call stack. Host APIs (timers, I/O, DOM) complete off that stack and enqueue callbacks. Concurrency is cooperative through the event loop. Workers are separate realms with their own loops, not extra stacks inside your function.
TypeScript does not run. It rejects programs before emit, and the emit settings decide the JavaScript and the .d.ts your consumers compile against. A bundler can strip types and still leave you with a broken moduleResolution.
Ask this before you reach for a framework explanation: which queue, which binding, which root, or which compiler flag produces the behavior in front of you?
Callbacks, promises, and async/await
All three styles schedule on the same event loop. async/await is syntax over promises. Promise reactions enqueue microtasks.
| Style | Control flow | Error path | Composition | Interview trap |
|---|---|---|---|---|
| Callbacks | Nested, inverted | Error-first argument | Hard to flatten | Forgot to forward err |
| Promises | Then-chains | Rejection plus catch | Thenable pipeline | Unhandled rejection |
| async/await | Reads like steps | try/catch | Sequential clarity | Missing await, or await inside a loop |
| Choice | What you get | What you pay |
|---|---|---|
await in a loop | One step finishes before the next starts | Latency adds up |
Promise.all | The independent calls overlap | One rejection fails the group unless you handle it |
The full queue rules, including Node's process.nextTick and the browser render chance, are Event Loop.
Structural and nominal typing
TypeScript's default is structural: a value is compatible when it has the members you asked for. Java, C#, and often Scala are nominal: the declared name is part of identity.
| Structural (TypeScript default) | Nominal (Java, C#, often Scala) | |
|---|---|---|
| Compatibility | Shape match | Same declared name or brand |
| Extra properties | Excess-property checks on fresh literals | Usually rejected by type identity |
| Brands | Phantom fields or unique symbols | Built-in classes |
| Rename | Safe when the shape holds | Breaks unless you keep an alias |
Fresh object literals get excess-property checks. A variable that already has a wider type generally does not. Brands (string & { readonly brand: unique symbol }) are how you recover a nominal flavor for IDs. Generics, conditionals, and mapped types are the type system page. How those types leave your package is modules and emit.
Proficiency stack
Read the chain top to bottom. The last step returns to this hub.
Flow
- 1
1. Language hub
- next2. Event loop
- 2
2. Event loop
- next3. Closures and this
- 3
3. Closures and this
- next4. Memory and GC
- 4
4. Memory and GC
- next5. Type system
- 5
5. Type system
- next6. Modules and emit
- 6
6. Modules and emit
- next1. Language hub
Lesson map
JavaScript & TypeScript — Intermediate to Advanced Proficiency
Hub: intermediate–advanced JS/TS language proficiency — event loop, closures/this/prototypes, memory/GC, TS types, modules/emit — not React/Next.
Architecture. Architecture
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB hub["1. Language hub"] el["2. Event loop"] cs["3. Closures and this"] mem["4. Memory and GC"] hub -->|1. Language hub to 2. Event loop| el el -->|2. Event loop to 3. Closures and this| cs cs -->|3. Closures and this| mem
What the rest of the cluster adds
- Event Loop — call stack, microtasks, macrotasks, and Node versus the browser.
- Closures, scope, this, and prototypes — lexical environments, binding rules, and
classas constructor sugar. - Memory, GC, and WeakRef — reachability, weak collections, and language-level deopts.
- TypeScript type system — generics, conditional types, mapped types, inference.
- Modules, emit, and strictness — ESM/CJS,
tsconfig,.d.ts, andpackage.jsonexports.
Heap-snapshot procedure and flame graphs live in the performance studies. This page only needs the reachability rule those tools confirm.
Queue order (run this)
The console program is the interview smoke test. Expected order: A sync, D sync, B microtask, C macrotask. The Run button uses a tiny scheduler so the log finishes in one turn. Paste the fenced snippet into a real console for the host queues.
console.log("A sync");
Promise.resolve().then(() => console.log("B microtask"));
setTimeout(() => console.log("C macrotask"), 0);
console.log("D sync");
// Expected: A, D, B, CPress Run. Snippets must be self-contained — no network, files, or native modules.
Sync work runs first, including the line after the timer was queued. The microtask drains before the macrotask. That single order explains most "why did this fire first?" questions.
Interview Q&A
What does JavaScript is single-threaded actually mean?
Answer
One call stack executes at a time per realm. Host APIs (timers, I/O, DOM) complete off that stack and enqueue callbacks. Concurrency is cooperative through the event loop, not through OS threads in user JavaScript. Workers are separate realms with their own loops.
Why learn TypeScript internals if Babel or SWC emit JavaScript anyway?
Answer
Emit settings, declaration files, and strictness are the public API guarantee. A wrong module, moduleResolution, or declaration breaks consumers. Weak types ship lies that still run. The bundler did not choose your .d.ts contract.
Where does React fit?
Answer
Outside this cluster. Hooks rely on closures and the event loop, but the framework API is a separate curriculum. If the question is "why did this then run first?", answer with queues.
Why does Promise.resolve().then often beat setTimeout of 0?
Answer
Then-callbacks are microtasks. Timers are macrotasks. After the current stack, the microtask queue drains before the next timer task. The event-loop page has the Node exceptions, including process.nextTick.
Is async/await a different runtime?
Answer
No. An async function returns a promise. await yields, and the resume is a microtask when the awaited thenable settles. await inside a for loop is sequential. Promise.all overlaps independent work.
Structural or nominal, in one sentence?
Answer
TypeScript asks "does this value have the members?" A nominal language asks "was this declared with that name?" Extra fields on a fresh literal are checked. Extra fields on an existing variable usually are not. Brands restore a nominal flavor for IDs.
What is the tradeoff of strictness?
Answer
strict and the flags under it catch nulls, implicit any, and sloppy function types before emit. The cost is ceremony and the occasional conditional type. Prefer a type that matches a domain invariant over a type that only quiets the compiler.
What do you say in the first minute?
Answer
One stack per realm. Microtasks (promises) drain before the next macrotask (timers and I/O). Closures capture environments. this is the call site, unless the function is an arrow. GC keeps what is still reachable. TypeScript is structural, and emit settings are part of the product.
Pitfalls
- Treating a zero-delay timer and a promise reaction as the same queue.
- Assuming a method closes over
this. - Holding DOM nodes or listeners from a closure that outlives the view.
- Reading TypeScript as if names were brands.
- Changing
modulewithout changingpackage.jsonexports. - Explaining a language bug with a component tree.
Write four lines: a sync log, a Promise.resolve().then log, a setTimeout log, and another sync log. Predict the order before you run them. Then name which later page you would open if the timer never runs, if this is undefined, or if a buffer survives the handler.