If you don’t already know what a .d.ts
file is, I recommend reading my other post on the topic.
Writing and managing types for a Typescript project is a cumbersome issue, and one that is often handled differently from project to project.
A common pattern that has evolved is to “place types where they are needed” or “colocate them with the model” (in fact this is my own rule too!), but many more options are possible.
Adding on top of that, not only are there many different locations to place your types, but the question can also expand to whether you should even use a regular .ts
file for them, or something else entirely?
Perhaps you are using a .d.ts
file to emit global types – but should you be?
This post seeks to address exactly that! So let’s dig in.
What is a .d.ts file?
First we need to agree on the basics. A .d.ts
file is simply a typescript convention for a file in which we store only types.
“Great, that sounds wonderful!” you may think, fantasizing that it is exactly what you need.
Hold up.
Unfortunately, it’s not that simple.
While .d.ts
files can be useful, they are actually not the recommended approach and can even be harmful, depending on your project and the code you are writing.
See, .d.ts
files are commonly used to generate a global output, making the types available anywhere in a project, so for that reason they should be used sparingly.
It is not advisable to put all your types in such files.
They were thought up in the early days of Typescript and worked in parity with .js
files, providing types in Javascript projects, which was a big upgrade at the time.
Now we have more efficient ways to do that, such as using certain settings in our ts configuration, eg checkJs: true
combined with allowJs: true
to start doing incremental project migrations, that completely superseed the need for any .d.ts
files.
Technically speaking, it is possible to achieve the same global type availability using a regular .ts
file, its just not used as often.
For convenience let’s refer to the concept as global type declaration. As the quick reader may be able to guestimate, there are several cases where this is actually a really good idea.
And we’ll get to those! But first, let’s take a closer look at when not to use these files.
When not to use a global type declaration
There are two rules against using global type declarations that I want you to remember. The first is a hard rule that you can never break. The second is one you should keep in mind as it will help you navigate when to choose a global type declaration as a solution.
Rules for determining not to use global type declaration files:
- When you are writing a library
- When your type does not need to be globally available, or can conveniently be imported
Let’s have a closer look.
When you are writing a library
Writing library code needs to live up to certain standards. Admittedly I’m no expert and have only written libraries for the learning experience of it, but Typescript has been flagged recently for being hard to adopt in these projects.
Whatever you do, do not resort to writing global types.
The reasoning here is very simple: Global types are just that; global. When another developer adopts your project, they are also importing all your global types, which can potentially cause a lot of harm if they have similarly named types already.
Not to mention the simple act of polluting the general space, making it confusing to know which type is necessary, which kind of defeats the point.
So repeat after me: “I shall not use global type declarations in a library code project”.
Thanks! 👏
When your type does not need to be globally available, or can conveniently be imported
The next rule is worth considering as a helpful “rule of thumb” that can help you navigate the challenging landscape of being a software developer.
Generally speaking, you want to put your types as close to where you need them, or where they logically belong.
If you believe in code colocation (I’m a big believer myself), its obvious that the type for a User
should be most likely located within a src/user(s)
folder, close to the model.
This helps answer whether a global type is necessary or not, because a “user”, while it may exist and often be relevant in your project, isn’t a type that needs to be globally available.
For example, in your src/blog/
folder, you may not need to reference a user at all, except for a user_id
, in which case we can answer that user isn’t globally necessary.
Yes, that is a small difference, but an important one, because overusing the option of defining global types will quickly pollute your project and confuse you. This will become extremely apparent as soon as a new developer jumps in and need to learn the project.
So, to keep things simple, make sure that most types are defined locally, close to their model and import them explicitly when needed.
So, when should I use global types?
While I won’t be going into too much detail about when to use global type declaration files (you can see my other post about those), I’ll make a few brief points to solidify the core concept.
There are plenty of reasons though, why using .d.ts
to setup global type declarations, or even local ones if need be!
Here are a few:
- You need to amend a global type, and a “definitely typed” package doesn’t exist
- Your project is in pure Javascript, but you want stronger typing
I have found myself needing to extend the Express Request
object in order to make a custom implementation of user sessions, that isn’t 1:1 supported by passport
(the authentication library).
While my need didn’t strictly necessitate a global type, it has made it easier to access the type, and is not polluting anything since we are writing app code, and not a library.
This is all very well documented in my other post, so you can refer to the instructions there 🙂
If this was insightful, sign up to my newsletter right here, to receive more instructions on .d.ts
files and other helpful typescript tips, right in your inbox 👇
Thank you for reading!