What you'll learn
TypeScript can make domain states and dependency contracts explicit, but network, environment, and database values remain unknown at runtime. Combine strict compilation with runtime validation and a correct ESM build.
By the end of this lesson, you'll be able to:
- Configure strict TypeScript and ESM
- Model domain states and errors
- Narrow unknown boundary input
- Ship source maps and declarations where needed
Core mental model
Node.js becomes easier when you separate the JavaScript language from the runtime and the operating-system capabilities it exposes. Use this table as a decision guide.
| Concept | What it means | Decision rule |
|---|---|---|
| unknown | A value requiring proof before use | Use at external boundaries instead of any |
| Discriminated union | Variants identified by a literal field | Model finite states and require exhaustive handling |
| Runtime schema | Executable validation of real values | Parse HTTP, config, queue, and persistence boundaries |
Professional workflow
Build and verify Node.js programs from the terminal in small, observable steps.
- Define the typed Node.js boundary boundary: inputs, outputs, invariants, ownership, and expected failures.
- Design the data or message contract before choosing implementation details.
- Implement the smallest correct path with dependencies passed explicitly.
- Add validation, failure translation, cleanup, and concurrency behavior.
- Verify the boundary with realistic data and at least one adversarial case.
- Measure or observe the behavior before optimizing or extracting abstractions.
Keep the feedback loop short
Guided code lab
Make invalid job states harder to express
A discriminant narrows the available fields and the never check makes new states a compile-time obligation.
type Job =
| { state: 'queued'; queuedAt: Date }
| { state: 'running'; startedAt: Date; progress: number }
| { state: 'failed'; failedAt: Date; reason: string }
| { state: 'complete'; completedAt: Date; resultKey: string };
function label(job: Job): string {
switch (job.state) {
case 'queued': return 'Waiting';
case 'running': return Math.round(job.progress * 100) + '%';
case 'failed': return job.reason;
case 'complete': return job.resultKey;
default: { const neverJob: never = job; return neverJob; }
}
}Production practice
Contract
Static types describe trusted in-process values; runtime schemas earn trust for anything crossing a process or storage boundary.
Verification
Run typecheck separately, test schema acceptance/rejection, verify emitted ESM imports, stack traces, declaration output, and package exports.
Operations
Pin compiler settings, publish source maps securely, avoid transpile-only production gates, and track validation failures by boundary.
Common failure mode
Independent workshop
Migrate one API vertical slice to strict TypeScript.
Your finished workshop must include:
- Strict tsconfig
- Domain unions
- Typed ports
- Runtime request/config schemas
- Typed errors
- Build/typecheck tests
Definition of done
Recap & quick check
Key takeaways
- Types disappear at runtime
- Unknown requires narrowing
- Unions model states
- Assertions do not validate
- Compiler and runtime checks cooperate
Quick check
1. What type fits untrusted JSON?
2. Does as TaskInput validate data?
3. Why use a discriminated union?
Next: Docker, Images & Local Infrastructure