Back to Latest PostsBlog

TypeScript Best Practices for Fullstack Development

By Carlos Diaz · November 22, 2024 · 3 min read

A production incident I remember clearly involved a function that received an order object with a status field that could theoretically be one of six strings, but the code only handled four of them. The other two were valid states we had simply forgotten about. That bug does not exist in a codebase that models state with discriminated unions instead of loose string types, and that is the pattern that has done more for my code quality than any other single TypeScript technique.

A discriminated union forces you to handle every case explicitly, because the compiler will not let a switch statement compile if a variant is missing and you have strict mode and exhaustiveness checks turned on. The same idea extends to domain modeling with branded types: an email address, a user ID, and a plain string all being represented as string is exactly how you end up passing the wrong value into the wrong function without either the compiler or a test catching it. Branding these types costs nothing at runtime and catches an entire category of mistakes at the type level.

Generics deserve more care than they usually get in application code. A loosely typed generic utility function that accepts any internally is barely more useful than no types at all - the value comes from constraining the generic tightly enough that the compiler can actually verify the function behaves correctly for every type that satisfies the constraint, not just the one you tested with.

The highest-leverage practice, though, is sharing types across the stack. When your API layer and your frontend consume the same generated types - whether through a monorepo package, an OpenAPI-generated client, or a GraphQL codegen setup - an API change that breaks the frontend shows up as a compile error in CI, not as a runtime bug a user reports in production. That single practice has caught more integration bugs on my teams than any amount of manual testing discipline ever did.

None of this is about chasing type safety for its own sake. Every one of these patterns exists because it removed a category of real bugs from a real codebase, and that is the bar I hold new TypeScript patterns to before I introduce them to a team.

Key Takeaways

  • Discriminated unions make illegal application states unrepresentable and force exhaustive handling.
  • Branded types add domain-level safety without any runtime overhead.
  • Strict generic constraints keep reusable utilities type-safe end to end, not just for the type you tested with.
  • Sharing generated types between frontend and backend catches integration bugs at compile time.
  • Every type-safety pattern should earn its place by removing a real category of bugs, not for its own sake.