All articles
TECHNOLOGY6 June 2025Rue Johnson

TypeScript: You Could Never Be Too Safe

Type safety is not extra work. It is less work, because you catch bugs at compile time instead of debugging them in production at 2 AM.

TechnologyTypeScriptFrontendBackend
TypeScript: You Could Never Be Too Safe

TypeScript

You Could Never Be Too Safe

Strict Mode or No Mode:

Every TypeScript project we start at MajorLinkx has strict: true in tsconfig.json from the first commit. Not strictNullChecks alone. Not noImplicitAny alone. Full strict mode. This means no implicit any types, no unchecked null/undefined access, no implicit returns, and strict function types. Teams that adopt TypeScript without strict mode are writing JavaScript with extra syntax. They get the verbosity of types without the safety guarantees. That is the worst of both worlds.

The pushback we hear is always the same: strict mode slows us down. It does, for about two weeks. After that, the compiler catches bugs that would have taken hours to debug in production. A function that expects a User object but receives undefined? The compiler catches it. A switch statement that does not handle every case in a discriminated union? The compiler catches it. An API response that might be null but your code assumes it is always present? The compiler catches it. The two weeks of adjustment pay for themselves the first time a type error prevents a production incident.

Generics, Discriminated Unions, and Type Guards:

TypeScript's type system is expressive enough to model real business logic, not just primitive types. Generics let you write functions and components that work with any type while preserving type safety: a function that fetches data from an API and returns the typed response, not an any blob you have to cast. Discriminated unions model state machines: a request is either Loading, Success with data, or Error with a message. Pattern matching with switch statements becomes exhaustive; the compiler refuses to compile if you forget a case.

Type guards let you narrow types at runtime without losing compile-time safety. The isUser(response) function returns a boolean at runtime but tells the compiler that within the true branch, response is definitely a User. This eliminates the as User type assertions scattered through most TypeScript codebases. Type assertions are lies you tell the compiler; type guards are proofs you provide to the compiler. We ban as casts in code review except for a handful of documented exceptions where third-party library types are genuinely wrong.

TypeScript on the Backend:

TypeScript on the frontend is table stakes. TypeScript on the backend is where large codebases either thrive or collapse. We run TypeScript with NestJS for API services, and the combination of decorators, dependency injection, and typed DTOs with class-validator means request validation, response serialization, and error handling are all type-safe from the HTTP boundary through to the database query. A malformed request body does not reach your service layer because the DTO validation rejects it at the controller.

Shared types between frontend and backend eliminate an entire class of integration bugs. When the API changes a response shape, the frontend build fails immediately, not after a user files a bug report. We publish shared type packages in our monorepos so both the NestJS API and the Next.js frontend import the same interfaces. Changing a field name in the shared package causes compile errors in every consumer, which means you find and fix every reference before deploying. This is the type safety multiplier: it compounds across your entire stack.

Our TypeScript Conventions:

Beyond strict mode, we enforce specific patterns. Interfaces over type aliases for object shapes because interfaces support declaration merging and provide clearer error messages. Explicit return types on exported functions so consumers see the contract without reading the implementation. Enums only for string unions that map to external values; otherwise, const objects with as const provide better type inference. Never export default; always use named exports so imports are grep-friendly and refactoring tools can trace references.

We also avoid overtyping. Not every variable needs an explicit annotation; TypeScript's inference engine is excellent. const name = 'Rue' does not need : string. But function signatures, API boundaries, and shared interfaces get explicit types because those are the contracts other code depends on. The goal is precision where it matters and inference where it is unambiguous. TypeScript should make your code safer without making it harder to read.