Ship Something Real
Expert · 180 min build · 🏆 milestone project
This is the TypeScript course capstone: a portfolio-grade project brief where you design types first, build the project in your own environment, validate every boundary, test values and types, and ship something you can explain. The goal is not to finish a toy scaffold here; it is to prove you can carry TypeScript from idea to release.
Pick one track and take it all the way. Keep the scope small enough to finish, but real enough that the type design matters.
What you'll build
Choose one track:
- Full-stack app — a small product workflow with a typed UI state, server/API boundary validation, typed persistence, and a demoable happy path. Examples: habit tracker, workshop signup, reading list, invoice mini-app.
- Typed library — a reusable package with a stable public API, declarations, type tests, docs, and a packed-artifact smoke test. Examples: config parser, date-range helper, form-state core, result/chaining utility.
- CLI tool — a command-line tool that parses flags/input into trusted command shapes, handles expected failures as data, and exits predictably. Examples: markdown checker, changelog helper, JSON report formatter, project audit tool.
- Type-safe SDK — a client for a public or local API shape that validates unknown responses, exposes typed operations, and documents failure modes. Examples: weather-like demo API, issue tracker API, finance sandbox, local fake service.
A capstone is a final project that combines the course skills into one coherent artifact. A milestone is a checkpoint with acceptance criteria you can verify yourself before moving on.
Start with a type-design sketch
Use this playground only as a warm-up. It helps you choose a track, name the project, and write the first typed promise. The real work happens in your own repository.
→ Type-safe SDK: Typed Issue Digest
→ Issue ids, statuses, API responses, and expected failures are typed before callers use them.
→ milestones: 6
Checkpoint 1: design the domain model
Start with the values your project is about. Do not begin with database columns, form fields, CLI strings, or API JSON. Begin with the trusted domain shapes your core logic should receive.
Acceptance criteria:
- You have a short
READMEparagraph describing the project and chosen track. - Core states use literal unions or discriminated unions where the set is closed.
- Same-shaped domain values that must not mix use brands after proof.
- Impossible combinations from 27.1 cannot be constructed without a type error.
- Public functions annotate boundaries and let inference handle local variables.
Checkpoint 2: validate every boundary
List every place untrusted data enters: forms, route params, API responses, files, environment variables, CLI flags, prompt answers, database rows, or package consumer input. Each boundary should parse into trusted values or return a typed failure.
Acceptance criteria:
- Boundary inputs start as
unknown, strings, or framework-provided raw shapes, not trusted domain types. - A schema, decoder, predicate, or constructor turns raw data into trusted values.
- Application/core code does not read fields from unvalidated
unknown. - Any local assertion is tied to nearby runtime proof.
- At least one invalid input case is covered by a test.
Checkpoint 3: model expected failure
Decide which failures are expected product behavior and which ones are broken invariants. Expected failure should be visible in the type system; broken invariants should fail loudly.
Acceptance criteria:
- Expected failures return a discriminated union such as
Result<T, E>. - Callers must check
ok,status, or another discriminant before using success data. - Error variants carry enough typed detail to render, log, or branch.
- Exhaustive switches or exhaustive records cover every failure kind.
- No expected workflow relies on catching a surprise throw.
Checkpoint 4: test runtime behavior and type promises
The capstone should prove both worlds: JavaScript behavior at runtime and TypeScript promises at compile time.
Acceptance criteria:
- Runtime tests cover the main happy path and at least two failure paths.
- Test fixtures use
satisfies, factories, or typed builders instead of broad assertions. - Public API promises have type tests that prove both accepted and rejected calls.
- The project has a visible
typecheckcommand usingtsc --noEmitor the project equivalent. - A fresh clone can install dependencies and run the checks from documented commands.
Checkpoint 5: wire tooling and architecture seams
Keep the core easy to test. Push framework, network, filesystem, database, and process details behind small typed seams.
Acceptance criteria:
- External systems are reached through typed ports/adapters or narrow wrapper modules.
- The core can run against a fake or in-memory adapter in tests.
tsconfigis strict enough to catch nullability, indexed access, and boundary mistakes.- Lint/format commands are documented and do not replace the compiler.
- CI or a local script runs typecheck, lint, runtime tests, type tests, and build in a clear order.
Checkpoint 6: document, release, and demo
Shipping means another person can understand what it does, run it, and trust the artifact they receive.
Acceptance criteria:
READMEincludes the problem, track, install/setup, usage, verification commands, and known limits.- Public APIs or commands have examples backed by checked code where practical.
- A library or SDK has declarations and a packed-artifact smoke test.
- A CLI has documented commands, exit behavior, and sample output.
- A full-stack app has a short demo script that exercises typed UI/server/data boundaries.
- You can explain every broad assertion, generated type, and runtime validation rule in your own words.
Stretch goals
Choose only after the base project is shippable:
- Add a second adapter: in-memory plus real file/database/API adapter.
- Add type-level tests for inference on a public helper.
- Add a small migration or versioned API change with a changelog note.
- Generate docs from exported types or checked examples.
- Measure typecheck time before and after one advanced helper, then simplify if the helper is not worth it.
- Ask a peer to review one boundary and one public API signature.
How to get unstuck
If the domain model feels loose, revisit Designing with Types, Discriminated Unions, and Branded Types & Assertion Functions. If boundary data is leaking inward, go back to Type Erasure & the Runtime Boundary and Schema Validation with Zod. If failures are noisy, compare your shape with Result Types: Errors as Values. If the project is hard to test, use API & Architecture Patterns to draw ports and adapters. If shipping feels vague, use Type-Checking in CI, Designing a Library's Public API, and Becoming a TypeScript Professional as your release checklist.
You have the full loop now: model the domain, guard the boundary, make failure explicit, test the promises, and ship the smallest useful thing that tells the truth.