First things first, TypeScript is a godsend. It has a powerful type system, with some really nice handling of falsy values. It’s a great tool for a not great situation.
For legacy projects separating type definitions from implementation can be an attractive option. We get to export types without the need to rewrite the source! However, this approach but it can lead to a number of problems. In this post, I’ll outline some of the reasons why you should avoid separating type definitions in your codebase.
The Problem with Separate Type Definitions
Separating type definitions is that it introduces an extra layer of maintenance overhead. There are now two files to maintain for every module in your codebase. That is two opportunities to make a mistake. Type definitions must be kept in sync with the implementation.
This can be particularly challenging when you have multiple developers working on the same codebase, as it’s easy for type definitions to fall out of date or for changes to be made to the implementation without updating the corresponding type definitions.
A more serious problem with separating type definitions is that it can lead to API mismatches between the actual code and the type definitions. This can happen when changes are made to the implementation but the corresponding type definitions are not updated to reflect those changes, or are not updated correctly.
For example, in the Sequelize version
6.6.5, there was a hook in the source code that was not included in the type definition file. This meant that any code that relied on the type definition would not be able to use that hook without disabling type checking for the entire configuration object.
It wasn’t until version 6.13.0, released 10 months later, that the hook was added to the type definition file.
This is a relatively minor issue, but it’s possible for more critical issues to sneak in. Is this type actually non-null? Does this property actually exist on this class? If the types are separate, there is no way to be certain until runtime… not great!
3. Increased Testing Burden
However, with separate type definitions, the situation is much worse. The type definitions can be completely incorrect. The exported values could be in different modules. Classes could have completely different sets of properties and functions in the source and in the type definition. An exhaustive test suite would have to cover all of these cases!
That is quite a burden! It’s worth asking, if we are writing tests to enforce types, what is the point of TypeScript?
Alternative Approach: Convert to TypeScript
To avoid the problems I outlined above I recommend the following approach for legacy projects:
1. Immediately adopt the TypeScript compiler.
These options activate the most useful checks that TypeScript provides: What is this thing? Can I count on it being there?
The latter is somewhat less useful since the introduction of optional chaining, but constant null checks can really clutter up code with unnecessary runtime logic. You can read more about the value of
Without these options, a lot of the value of TypeScript is left on the table.
.js to files
.ts one by one.
- Change the file suffix from
- Define types as you go. This means adding type annotations to variables, parameters, and function return types. If you’re not sure what type to use, start with
anyand come back to it later.
@ts-ignoreif you encounter a strict null check error that you can’t resolve right away. This will allow you to keep working on the file without TypeScript complaining about the null check.
4. Remove uses of
Lastly you’ll want to remove the uses of
typescript-eslintto lint your TypeScript code. It’s a nice extension to the built in TS rules.
ban-ts-comment. This will prevent you from using
@ts-ignorein your code.
You can always disable or remove these rules temporarily or inline until you’ve cleaned out your entire code base.
The conversion process can be tedious, but it avoids the problems associated with separate type definitions and big bang updates that risk unknown and untested updates. Unless the maintainers of the project are unable or unwilling to convert the project, separate type definitions should be avoided.