← Back to blog Post

Type composition in TypeScript

Hosted here as an English translation. Originally published on Lean Mind.

Introduction

One of TypeScript’s strongest features is the flexibility it offers when defining data structures. But that same flexibility can also become a major source of trouble when it is used without enough precision.

The problem

The example starts with a UserAddress type that initially seemed enough to represent a user’s address. Over time, new requirements appeared: the address could be for billing or shipping, and shipping could itself be either physical or digital.

The quick solution was to turn many properties into optional ones so a single type could cover every case. The downside is that this makes impossible states easy to represent, and that leads to strange bugs, such as mixing physical-shipping fields into a digital-shipping case.

The solution: be more specific

Instead of over-generalizing, the proposal is to decompose the problem:

  • Customer for the basic user data.
  • UserDigitalAddress for cases that use activationEmail.
  • UserPhysicalAddress for cases that contain a physical Address.
  • UserDigitalPhysicalAddress for the mixed case.

From there, we can compose those types using Omit, intersections, and unions to express the domain rules more precisely.

Result

The point is not using advanced types for their own sake, but using them to rule out impossible scenarios and make the code more semantic. Instead of filling everything with optional properties, we define structures that reflect the actual domain constraints.

Conclusion

Type composition is not a universal answer, but in many cases it protects us from errors caused by overly permissive models. The result is code that reads more clearly and a type system that actively supports the intended behavior during development.

  • typescript
  • type-systems
  • design