Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Low-Level Design (Object-Oriented Design)  Module 9 – One design, six languages

LLD in TypeScript with erasable syntax only

Write a low-level design in TypeScript that Node.js 24 runs as it is: unions instead of enums, declared fields, #private state and checks that hold.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7 and Node.js 24.21.0
  • By MySmartCoPilot
Free intro, Pro: 8 of 30 minutes free
Free: Erasable syntax in one example. With a pass: the other 8 sections and 3 interview answers, about 22 minutes.

What you will learn

  • Model closed sets with union types and discriminated unions instead of enums
  • Write classes that run unchanged under Node.js 24 type stripping
  • Choose between TypeScript private and JavaScript
  • Design identifiers, modules and cleanup that survive type stripping

Before you start

What you’ll learn · Before you start

What you will learn

  • Model closed sets with union types and discriminated unions instead of enums
  • Write classes that run unchanged under Node.js 24 type stripping
  • Choose between TypeScript private and JavaScript
  • Design identifiers, modules and cleanup that survive type stripping

Before you start

On this page

A low-level design in TypeScript can run on Node.js 24 exactly as written, with no build step, if it uses only erasable syntax: TypeScript that becomes plain JavaScript when its type annotations are deleted. Node.js runs a .ts file by replacing every type with whitespace, a feature that has been stable since Node.js 24.12.0, and it checks none of the types. Syntax that needs generated JavaScript cannot be deleted, so Node.js refuses it: an enum, a namespace that holds code, a parameter property such as constructor(private seat: string), and an import x = require() alias. TypeScript 5.8 added --erasableSyntaxOnly, which makes tsc report the same constructs as error TS1294 before anything runs. The limits cost a design little. A closed set becomes a union of string literals, a lifecycle becomes a discriminated union and a table, a class declares its fields and assigns them in its constructor, and hidden state goes into #private fields that the JavaScript engine enforces.

Every example in this lesson ran on Node.js 24.21.0, and every file type-checks with TypeScript 6.0.3 under --strict and --erasableSyntaxOnly, except the ones that are meant to fail.

Erasable syntax in one example

Flow: Node.js strips the types of a .ts file and runs it; tsc checks the types; both refuse syntax that needs generated code.booking.tsclasses, unions, interfaces, branded IDsnode booking.ts(Node.js 24.21.0)types replaced by whitespace,nothing is checkedplain JavaScript runstsc --noEmit --erasableSyntaxOnly(TypeScript 6.0.3)type errors, and TS1294 for syntaxthat needs generated codeneeds generated JavaScript, so both refuse it:enum · namespace with code ·parameter properties · import = aliasto run itto check itERR_UNSUPPORTED_TYPESCRIPT_SYNTAXTS1294

One TypeScript file, two tools: Node.js runs it without checking, tsc checks it without running

Text description of the diagram

A flow diagram with one source file at the top, booking.ts, which holds classes, unions, interfaces and branded IDs.

  • The path "to run it" goes to node booking.ts on Node.js 24.21.0, then to "types replaced by whitespace, nothing is checked", then to "plain JavaScript runs".
  • The dashed path "to check it" goes to tsc --noEmit --erasableSyntaxOnly on TypeScript 6.0.3, then to "type errors, and TS1294 for syntax that needs generated code".
  • Both paths also lead to a note: enum, namespace with code, parameter properties and import = alias need generated JavaScript, so both tools refuse them. The line from Node.js is labelled ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX and the line from tsc is labelled TS1294.

Seat classes are a classic closed set. Written as an enum, they are a runtime object that the compiler would have to generate. Written as an object of constants marked as const, plus a union type derived from it, they are ordinary JavaScript once the types are gone:

Seat classes as an enum, then in erasable syntax

session · enums_session.mjs

import { node, version } from './session.mjs';

// The enum version is refused before anything runs; the erasable version runs as it is.
version();
node('enums/seat_class_enum.ts');
node('enums/seat_class.ts');

Output

# Node.js v24.21.0
$ node enums/seat_class_enum.ts
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
[exit status 1]
$ node enums/seat_class.ts
[ 'standard', 'premium', 'recliner' ] 5000 15000

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node enums_session.mjs

enums/seat_class_enum.ts

// Seat classes as a TypeScript enum. An enum is a runtime object that the compiler generates,
// so there is nothing Node.js could strip: it refuses the file.
enum SeatClass {
  Standard = "standard",
  Premium = "premium",
  Recliner = "recliner",
}

function surcharge(seatClass: SeatClass): number {
  return seatClass === SeatClass.Recliner ? 15000 : seatClass === SeatClass.Premium ? 5000 : 0;
}

console.log(surcharge(SeatClass.Premium));

enums/seat_class.ts

// The same closed set in erasable syntax: an object of constants marked as const, and a union type
// derived from it. Strip the types and plain JavaScript is left.
export const SeatClass = {
  Standard: "standard",
  Premium: "premium",
  Recliner: "recliner",
} as const;
export type SeatClass = (typeof SeatClass)[keyof typeof SeatClass];

// One row per class: tsc refuses this table when a class is missing (satisfies).
const SURCHARGE_PAISE = {
  standard: 0,
  premium: 5000,
  recliner: 15000,
} as const satisfies Record<SeatClass, number>;

export function surcharge(seatClass: SeatClass): number {
  return SURCHARGE_PAISE[seatClass];
}

console.log(Object.values(SeatClass), surcharge(SeatClass.Premium), surcharge("recliner"));

session.mjs

// Runs TypeScript files the way a terminal shows them: "$ node <file>", then what the program printed. Node.js runs a
// .ts file by stripping its types and checks none of them. When Node.js refuses a file or the program throws, only the
// error line is shown, followed by the exit status.
import { spawnSync } from 'node:child_process';

export function version() {
  console.log(`# Node.js ${process.version}`);
}

export function node(file) {
  console.log(`$ node ${file}`);
  const run = spawnSync(process.execPath, [file], { encoding: 'utf8' });
  process.stdout.write(run.stdout);
  if (run.status !== 0) {
    const lines = run.stderr.split('\n');
    console.log(lines.find((line) => /^\w*Error\b/.test(line)) ?? lines.filter(Boolean).at(-1));
    console.log(`[exit status ${run.status}]`);
  }
  return run.status;
}

The enum version never starts: Node.js stops at enum SeatClass with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX. The rewrite keeps what callers used. SeatClass.Premium is still a name, the type SeatClass accepts exactly the three strings, and surcharge("recliner") type-checks while surcharge("vip") would not. The price table ends in satisfies Record<SeatClass, number>, so a fourth class added without a price fails the type check.

In a machine-coding round this saves more time than it seems to. A design that runs with node design.ts needs no bundler, no compile step and no configuration, so the first run happens minutes into the round, and the same files run in the tests. Keep the type checker in the loop anyway, because Node.js runs code whose types are wrong without a word.

The rest of a TypeScript design follows from the same rule:

  • Closed sets and lifecycles are unions of literals plus tables that tsc checks for a missing state.
  • Private state goes into #private fields, because TypeScript’s private disappears with the other types.
  • Branded IDs make a ShowId and a SeatId two different types, although both are strings at run time.
  • Modules import types with the type keyword, or Node.js looks for them as values that do not exist.
  • Cleanup that must always happen goes into a using declaration with a [Symbol.dispose]() method.

Check yourself

3 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 3 Which of these declarations does Node.js 24 run unchanged when it strips the types of a .ts file?

    Choose one answer.

    Show the answer to question 1

    Answer: const SeatClass = { Premium: "premium" } as const;

    Deleting "as const" leaves an ordinary object, so the first line is erasable. An enum, a parameter property and a namespace that holds code all need JavaScript that the compiler would generate, so Node.js refuses them.

  2. Question 2 of 3 A .ts file passes the wrong type of argument to a function. What does node file.ts do?

    Choose one answer.

    Show the answer to question 2

    Answer: It runs the file, because type stripping checks no types

    Node.js replaces types with whitespace and checks none of them. Type errors are the job of tsc, which is why a design keeps the type checker in its loop even when Node.js runs the files.

  3. Question 3 of 3 Which tsc option reports enums and parameter properties as error TS1294 before anything runs?

    Choose one answer.

    Show the answer to question 3

    Answer: --erasableSyntaxOnly

    TypeScript 5.8 added --erasableSyntaxOnly to report the syntax that type stripping cannot handle, so the mistake is caught where the code is written instead of when Node.js starts.

Closed sets and lifecycles that tsc checks Pro: opens with a Pro, Premium or Ultimate pass

In this part:

  • TypeScript private against #private, measured
  • Branded IDs: a ShowId that cannot pass as a SeatId
  • Modules under type stripping: import type and file extensions
  • Cleanup that cannot be skipped: using and Symbol.dispose
  • Practise: rewrite a price list in erasable syntax
  • Interview questions
    • What is the difference between TypeScript's private modifier and a JavaScript #private field?
    • A TypeScript service has 40 enums and parameter properties in most classes, and the team wants to run it directly on Node.js 24 without a build step. How do you plan the migration, and what replaces each construct?
    • In a TypeScript booking service, a ShowId was passed where a SeatId was expected, and two customers were given the same seat. How do you design the identifiers so that this cannot compile, and where do you check them at run time?
    • A checkout holds seats, charges a card and then confirms. Every hold must end, whether the charge succeeds, is declined or throws. How do you design this in TypeScript on Node.js 24, and what can still leave a seat held?
  • Key takeaways

6 runnable examples (book.ts, book_plain_import.ts, booking.ts, check.ts, holds.ts, ids.ts, pricing.ts, seat.ts, seat_states.ts, session) · 3 interview answers · about 17 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Read a complete sample first: Low-level design: parking lot (the model)

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.