Why TypeScript Has Become the Standard
TypeScript's growth over the past several years has been remarkable, and by late 2023, it has firmly established itself as the default choice for professional web development teams working on projects of any meaningful scale. What began as an optional type layer atop JavaScript has become the language in which the majority of new front-end and full-stack projects are started.
The reasons for this dominance are straightforward and well-validated by industry experience. TypeScript catches entire categories of errors at compile time that would otherwise surface at runtime — often in production, in front of users. It provides vastly superior tooling, including autocompletion, refactoring support, and inline documentation. And it makes large, evolving codebases significantly more maintainable by making the relationships between components explicit and compiler-verifiable.
The annual State of JS surveys have consistently shown TypeScript's usage and satisfaction ratings climbing year on year. Major frameworks including Angular (which adopted TypeScript from its inception), React, Vue, and Svelte have all embraced TypeScript as a first-class citizen, and the npm ecosystem has seen a dramatic increase in packages shipping type definitions.
Start with Strict Mode
One of the most impactful decisions a team can make is enabling strict mode in their TypeScript configuration from the very outset of a project. The `strict` flag in the TypeScript configuration enables a collection of stricter type-checking options that catch a broader range of potential issues.
What Strict Mode Enables
The strict flag is actually a shorthand for enabling several individual options simultaneously, including strict null checks, strict function types, strict property initialisation, and the prohibition of implicit any types. Together, these options enforce a level of type safety that prevents many common categories of bugs.
The Case for Starting Strict
Whilst strict mode can feel onerous at first — particularly for developers transitioning from JavaScript who are accustomed to the language's flexibility — the investment pays dividends quickly. Strict mode prevents entire categories of null reference errors, ensures function signatures are honoured, and forces developers to be explicit about their intentions.
Critically, retrofitting strict mode onto an existing codebase is considerably more painful and time-consuming than starting with it enabled. Each stricter option, when enabled on an existing codebase, typically surfaces hundreds or thousands of errors that must be addressed. Starting strict from day one avoids this entirely.
Prefer Interfaces for Object Shapes
When defining the shape of objects, prefer the `interface` keyword over `type` aliases for most cases. Interfaces are extendable through declaration merging, produce clearer and more readable error messages when type mismatches occur, and better express the intent of defining a contract for an object's structure.
Reserve `type` aliases for union types, intersection types, mapped types, conditional types, and other cases where `interface` syntax cannot be used. This distinction helps maintain clarity and consistency across a codebase, and it makes it immediately apparent when reading the code whether a definition represents an object contract or a more complex type construction.
Embrace Discriminated Unions
Discriminated unions are one of TypeScript's most powerful features for modelling domain logic safely and expressively. By including a common literal property (the discriminant) across the members of a union, you enable the TypeScript compiler to narrow types automatically within control flow statements and ensure exhaustive handling of all possible cases.
Practical Applications
This pattern is particularly valuable for representing:
- Application states such as loading, success, and error, where each state carries different associated data
- Event types in event-driven architectures
- API response shapes that vary depending on the outcome
- Any domain concept where an entity can take one of several distinct forms with different properties
Exhaustiveness Checking
One of the most valuable aspects of discriminated unions is the ability to perform exhaustiveness checking. By handling each variant explicitly and adding a default case that assigns the discriminant to a never type, the compiler will produce an error if a new variant is added to the union without being handled. This compile-time guarantee prevents the category of bugs where new states or events are added but not properly accounted for throughout the codebase.
Avoid Overusing any
The `any` type effectively disables type checking for a value, undermining the primary benefit of using TypeScript in the first place. Whilst there are rare situations where `any` is genuinely necessary — typically at boundaries with untyped JavaScript libraries or when dealing with highly dynamic data structures — it should be treated as a last resort rather than a convenient escape hatch.
Prefer unknown Over any
When the type of a value is truly unknown at the point of definition, use the `unknown` type instead of `any`. Unlike `any`, `unknown` requires you to perform type checking or type assertion before using the value, maintaining type safety throughout your code whilst still accommodating genuinely dynamic values.
Tracking and Reducing any Usage
Consider adding a linting rule that flags any usage, requiring an explanatory comment for each instance. This creates a natural friction against casual use and provides documentation for future developers about why type safety was relaxed in that specific location. Over time, teams should actively work to reduce the number of any usages in their codebase as libraries gain type definitions and patterns become clearer.
Leverage Built-In Utility Types
TypeScript provides a rich set of built-in utility types that reduce duplication, express intent clearly, and prevent the common mistake of manually redefining subsets of existing types. Key utility types that every team should be fluent with include:
- `Partial` for making all properties optional, useful for update operations
- `Required` for making all properties mandatory
- `Pick` and `Omit` for selecting or excluding specific properties from a type
- `Record` for defining object types with known key and value types
- `Readonly` for preventing mutation of properties
- `ReturnType` and `Parameters` for extracting types from function signatures
Familiarity with these utilities prevents teams from reinventing the wheel and produces more maintainable code that clearly communicates its relationship to existing type definitions.
Write Meaningful Type Names
Type names should communicate intent and domain meaning. Avoid generic names such as Data, Info, Result, or Item in favour of descriptive names that convey what the type represents in your specific domain. Good type names serve as documentation in their own right and make code self-explanatory when reading through unfamiliar modules.
Consider adopting naming conventions that distinguish between different kinds of types — for example, suffixing props types with Props, response types with Response, and event types with Event. Consistency in naming conventions reduces cognitive load and makes the codebase more navigable.
Organise Types Thoughtfully
For larger projects, establishing a clear convention for where types are defined prevents the common problems of duplication, inconsistency, and difficulty finding the right type definition.
Colocation as the Default
Colocating types with the modules that use them tends to work better than centralising all types in a single file. When a type is used only within a single module or feature, defining it alongside the code that uses it keeps related concerns together and makes the code easier to understand.
Shared Types
Types that are used across multiple modules or features benefit from a dedicated shared location — typically a types directory or a set of type definition files organised by domain area. The key is ensuring these shared types represent genuine shared contracts rather than becoming a dumping ground for every type in the project.
Our Approach
At GRDJ Technology, TypeScript has been central to our development practice for several years, and these best practices reflect lessons learned across numerous client projects of varying scale and complexity. We find consistently that teams which adopt these practices early in a project spend significantly less time debugging type-related issues and more time delivering features that matter to their users and stakeholders.