TypeScript's type system is extraordinarily powerful, but power without discipline leads to codebases that are harder to work with than plain JavaScript. At Fyutrex, we've shipped over 50 production TypeScript projects and have converged on a set of patterns that we use in every single one. These aren't clever type gymnastics — they're practical patterns that make real teams ship faster with fewer bugs.
Branded Types for Domain Safety
The most impactful TypeScript pattern we use is branded types. They prevent entire categories of bugs by making it impossible to accidentally swap values that share the same primitive type.
Branded Types Example
type Brand<T, B extends string> = T & { __brand: B }
type UserId = Brand<string, 'UserId'>
type OrderId = Brand<string, 'OrderId'>
function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }
// This won't compile — you can't pass an OrderId where a UserId is expected
const orderId = 'abc' as OrderId
getUser(orderId) // ← Type error!The Result Pattern Over Exceptions
We never throw exceptions for expected error cases. Instead, we use a Result pattern that forces callers to handle both success and failure paths. This eliminates an entire class of unhandled error bugs and makes error handling explicit in every function signature.
The key insight is that exceptions should be reserved for truly exceptional situations — programmer errors, infrastructure failures, things that can't be meaningfully handled by the caller. Business logic errors (validation failures, not-found, permission denied) should be return values.
Pro Tip
Every project starts with these compiler options: strict: true, noUncheckedIndexedAccess: true, exactOptionalPropertyTypes: true, and noImplicitOverride: true. The first week with these settings is painful. Every week after that, they prevent bugs that would have taken hours to track down.
Discriminated Unions for State Machines
Every piece of state in our applications that can be in multiple states is modelled as a discriminated union. This pattern eliminates impossible states at the type level and makes exhaustive checking trivial.
We use this for API response states, form states, auth states, and any workflow that has distinct phases. The TypeScript compiler becomes your state machine verifier — if you forget to handle a state, it tells you at build time, not at runtime in production.
Zod at Every Boundary
We validate data at every system boundary using Zod schemas that double as TypeScript types. API responses, form inputs, environment variables, configuration files — all validated at runtime with schemas that generate their TypeScript types automatically. This 'parse, don't validate' approach means we trust our types throughout the application because we've verified the data at the edges.
Conclusion
These patterns aren't revolutionary individually. Their power comes from consistent application across an entire codebase. When every team member uses the same patterns, code reviews are faster, onboarding is smoother, and bugs are caught before they reach production.
Written by
Lead Engineer at Fyutrex
Alex is a senior full-stack engineer at Fyutrex with deep expertise in Next.js, TypeScript, and cloud-native architecture.
More from Alex