
Clean code in TypeScript: principles for maintainable projects
Practical clean code rules for TypeScript: strict types, clear names, small functions and validated boundaries, with examples and the mistakes to avoid.
In short: Clean code in TypeScript mostly means letting the type system work for you: turn on strict, avoid any, validate data at system boundaries and make impossible states impossible to represent. On top of that come the general rules that apply in any language: clear names, small functions that do one thing, and code organised by business domain rather than by technical layer. The result is software you can still change safely years later.
Why clean code in TypeScript deserves its own discussion
The classic clean code principles come from Java and C# projects. Much of that still applies, but some of it fits TypeScript poorly. Deep class hierarchies and interfaces with a single implementation tend to make TypeScript harder to read. In return, the language offers tools others lack: structural typing, union types, type narrowing and type inference.
Clean TypeScript uses these tools to move errors from runtime into the editor. Every bug the compiler catches is one no user ever sees.
The type system as a safety net
Strict mode from the start
The strict option in tsconfig.json enables, among other things, strictNullChecks and noImplicitAny. Without it, TypeScript is far more lenient and many bugs slip through. In new projects, strict belongs there from day one. A useful addition is noUncheckedIndexedAccess, which treats array and object lookups as possibly undefined.
Avoid any, use unknown
any switches off type checking for a value completely, and it spreads: everything derived from it is unchecked too. Where a type really is unknown, unknown is the right choice. The compiler then forces you to check the value before using it.
Make impossible states impossible
A common pattern is an object with many optional fields where not every combination is valid. A discriminated union is better:
type Request =
| { status: "loading" }
| { status: "success"; data: Order[] }
| { status: "error"; message: string };
Now no state can have both data and message. In a switch on status, the compiler knows in every branch which fields exist.
Validate data at the boundaries
Types exist only at development time. Whatever comes from an API, a form or a database is unchecked at runtime, whatever the type annotation claims. Libraries such as Zod or Valibot check incoming data against a schema and derive the TypeScript type from it, so the type and reality match. How to define interfaces clearly from the outset is covered in our complete guide to full-stack development with TypeScript.
Readability in daily work
Names that explain
A function called handleData tells you nothing. calculateShippingCost tells you what happens. Booleans read best as questions: isPaid, hasAccess. Abbreviations save a few seconds when typing and cost time every time someone reads them.
Small functions with one job
A function that loads, filters, converts and displays data is hard to test and hard to change. Split it along its tasks. As a rule of thumb, if you need the word "and" to describe a function, it probably does too much.
Explicit types on public interfaces
Inside a function, type inference does a good job and explicit annotations are often noise. On exported functions and module boundaries, explicit return types are worth it. They document the contract and stop an internal change from silently altering the public interface.
Organise by domain
Folders like components, utils and services turn into dumping grounds in larger projects. Grouping by business area, such as orders, customers and billing, keeps together what changes together.
Common mistakes in TypeScript projects
The most common mistake is generous use of as. A type assertion tells the compiler "trust me", and that is exactly where runtime errors come from later. There is almost always a better option: validation, type guards or properly typed functions.
Equally common are overly clever types. Generic constructions several levels deep look impressive, but if nobody on the team understands them, they are a burden. Types should make code easier to understand.
Finally, clean code without tests is only a claim. Tests are what give you the confidence to refactor without breaking behaviour. Which kinds of tests suit which purpose is covered in testing strategies for React applications. When cleaning up pays off commercially is the topic of technical debt in startups.
Frequently asked questions (FAQ)
Is strict mode worth it in an existing project?
Yes, but gradually. Enable the individual options one at a time and fix the errors module by module. That way the project keeps running during the migration.
Should I use type or interface in TypeScript?
In most cases they are equivalent. You need type for union types and derived types, while interface is common for extendable object shapes. A consistent team rule matters more than the choice itself.
Which tools help enforce clean code?
ESLint with the TypeScript rules, a formatter such as Prettier, and a CI pipeline that runs type checking, linting and tests on every pull request. Style questions are then handled by the machine, and reviews can focus on logic.
Conclusion
Clean TypeScript comes less from rules in books than from consistent use of the type system and good team habits. If you validate data at the boundaries, avoid any and as, and organise code by domain, you skip a large share of the debugging that would otherwise come later.
If you want to set up a TypeScript project or clean up an existing one, talk to us about web development.
Related posts
- React vs. Vue vs. Svelte: A Frontend Framework Comparison
React, Vue or Svelte for your next web project? A sober comparison of concepts, ecosystem, performance, hiring and long-term maintainability.
- Why we chose Next.js as our default web framework
Why we build web apps with Next.js by default: rendering options, SEO, TypeScript, self-hosting in Germany, and when we deliberately choose otherwise.
- Testing React Applications: Unit, Integration and E2E Tests
How to test React applications sensibly: which tests pay off, which tools fit, and how a test strategy catches bugs early instead of slowing the team down.
- Monorepos with Turborepo: Managing Multi-Package Projects
When a monorepo pays off, how Turborepo speeds up builds with caching and task graphs, and how to structure web apps, APIs and shared packages sensibly.