TypeScript pays off when it makes invalid states hard to represent and keeps runtime data honest. It pays off less when it becomes a second, decorative language full of any and type assertions.
Key takeaways
- Use
unknownat boundaries, then validate with a schema before trusting the data. - Discriminated unions model loading, success and error states better than optional fields.
satisfieschecks a value against a type without widening it.- Prefer the simplest boundary that keeps data and authorisation on the server.
What matters in practice
- Strict mode catches null and implicit-any bugs that would otherwise reach production.
- Types disappear at runtime; API, form and webhook data still need validation.
How to implement it
- Turn on strict mode. Enable
strictand fix the errors in small batches. - Type the boundaries. API responses, form data, environment variables and webhook payloads.
- Model states as unions. Replace
isLoading/error/databooleans with one discriminated union. - Use generics for reusable contracts. Keep constraints narrow and the API readable.
- Add runtime validation. Use Zod or a similar schema at the edge of the system.
- Keep the compiler fast. Use project references and avoid huge union types where they hurt tooling.
A working example
type Result<T> =
| { status: "loading" }
| { status: "error"; message: string }
| { status: "success"; data: T };
function render(result: Result<User>) {
if (result.status === "loading") return <Spinner />;
if (result.status === "error") return <Error message={result.message} />;
return <Profile user={result.data} />;
}
Production checklist
| Area | Check |
|---|---|
| Compiler | strict, noUncheckedIndexedAccess, noImplicitOverride |
| Boundaries | Zod schema for API, forms, env and webhooks |
| State | Discriminated unions instead of optional booleans |
| Exports | No any in public types; use unknown + validation |
| Tooling | ESLint type-aware rules and a fast tsc --noEmit |
| Tests | Type tests for complex generics where they matter |
Common mistakes
- Casting with
asto silence an error instead of fixing the model. - Using
anyin a shared type and leaking it everywhere. - Validating only on the client and trusting the server payload.
- Turning on every strict flag at once in a large codebase without a plan.
When this is the wrong tool
Every tool has a boundary. If the team is small, the use case is simple and the operational cost is high, a simpler stack may be better. The right choice is the one you can debug, secure and hand over without the original author.
Rollout plan
- Prove one slice. Use TypeScript for one real route or feature, not the whole product.
- Set the boundary. Write down what stays on the server, what runs on the client and how data crosses the boundary.
- Add the safety net. Types, validation, tests and error handling around the new slice.
- Measure. Check bundle size, latency, error rate and the developer experience before expanding.
- Document and hand over. Record the pattern, the pitfalls and the version constraints.
Testing strategy
| Layer | What to test | Why |
|---|---|---|
| Types | Strict compile and no unjustified any | Catches contract drift early |
| Unit | Pure logic, validation and state transitions | Fast feedback |
| Integration | API, database and auth boundaries | Most production failures live here |
| End-to-end | The critical user journey | Proves the wiring |
| Operations | Logs, errors, retries and rollback | A release is not finished at deploy |
Operational checklist
- Pin the runtime and dependency versions; test upgrades in a branch.
- Keep secrets on the server and validate every external payload.
- Add structured logs with a request ID, but never log tokens or personal data.
- Set a performance or bundle budget and check it in CI.
- Write a short runbook for the most likely failure.
Frequently asked questions
Yes, if the project will change. The cost is front-loaded; the benefit is fewer runtime surprises and better editor support. For a one-page static site, it may be optional.
Either works for most object shapes. Use interface for extendable public contracts and type for unions, intersections and mapped types. Consistency matters more than the choice.
At minimum strict. Add noUncheckedIndexedAccess and noImplicitOverride when the codebase can absorb them. Measure the error count and migrate in slices.
Yes. TypeScript cannot validate a JSON response, a form post or a webhook. Use a schema at every trust boundary.
Sources and further reading
- TypeScript handbook (typescriptlang.org)
- TypeScript strict compiler options (typescriptlang.org)
- Zod documentation (zod.dev)
Related reading
Need an implementation review?
Send the repository or the design. I will review the boundary, the failure modes and the parts that will be expensive to change later.
How to cite this guide: Nur Hossain Repon, “TypeScript best practices: complete developer guide (2026)”, Nhrepon Portfolio, reviewed October 2026, https://nhrepon.com/posts/typescript-best-practices-complete-developer-guide-2026.





Comments
No comments yet.