Deriving vs Decoupling: When NOT to be a Typescript Wizard
devvie
@devvie
One of the most common pieces of advice for writing maintainable code is to "Keep code DRY", or more explicitly, "Don't Repeat Yourself".
One way to do this in JavaScript is to take repeating code and capture it in functions or variables. These variables and functions can be reused, composed and combined in different ways to create new functionality.
In TypeScript, we can apply this same principle to types.
What Is A Derived Type?
A derived type is a type which relies on, or inherits from, a structure of another type. We can create derived types using some of the tools we've used so far.
We could use interface extends
to make one interface inherit from another:
AlbumDetails
inherits all of the properties of Album
. This means that any changes to Album
will trickle down to AlbumDetails
. AlbumDetails
is derived from Album
.
Another example is a union type.
A derived type represents a relationship. That relationship is one-way. Shape
can't go back and modify Triangle
or Rectangle
. But any changes to Triangle
and Rectangle
will ripple through to Shape
.
Deriving vs Decoupling
When you derive a type from a source, you're coupling the derived type to that source. If you derive a type from another derived type, this can create long chains of coupling throughout your app that can be hard to manage.
When Decoupling Makes Sense
Let's imagine we have a User
type in a db.ts
file:
We'll say for this example that we're using a component-based framework like React, Vue or Svelte. We have a AvatarImage
component that renders an image of the user. We could pass in the User
type directly:
But as it turns out, we're only using the imageUrl
and name
properties from the User
type. It's a good idea to make your functions and components only require the data they need to run. This helps prevent you from passing around unnecessary data.
Let's try deriving. We'll create a new type called AvatarImageProps
that only includes the properties we need:
But let's think for a moment. We've now coupled the AvatarImageProps
type to the User type. AvatarImageProps
now not only depends on the shape of User
, but its existence in the db.ts
file. This means if we ever move the location of the User
type, or split it into separate interfaces, we'll need to think about AvatarImageProps.
Let's try the other way around. Instead of deriving AvatarImageProps from User
, we'll decouple them. We'll create a new type which just has the properties we need:
Now, AvatarImageProps
is decoupled from User
. We can move User
around, split it into separate interfaces, or even delete it, and AvatarImageProps
will be unaffected.
In this particular case, decoupling feels like the right choice. This is because User
and AvatarImage
are separate concerns. User
is a data type, while AvatarImage
is a UI component. They have different responsibilities and different reasons to change. By decoupling them, AvatarImage becomes more portable and easier to maintain.
What can make decoupling a difficult decision is that deriving can make you feel 'clever'. Pick
tempts us because it uses a more advanced feature of TypeScript, which makes us feel good for applying the knowledge we've gained. But often, it's smarter to do the simple thing, and keep your types decoupled.
When Deriving Makes Sense
Deriving makes most sense when the code you're coupling shares a common concern. The examples in this chapter are good examples of this. Our as const
object, for instance:
Here, AlbumType
is derived from albumTypes
. If we were to decouple it, we'd have to maintain two closely related sources of truth:
Because both AlbumType
and albumTypes
are closely related, deriving AlbumType from albumTypes
makes sense.
Another example is when one type is directly related to another. For instance, our User
type might have a UserWithoutId
type derived from it:
Again, these concerns are closely related. Decoupling them would make our code harder to maintain and introduce more busywork into our codebase.
The decision to derive or decouple is all about reducing your future workload.
Are the two types so related that updates to one will need to ripple to the other? Derive.
Are they so unrelated that coupling them could result in more work down the line? Decouple.