Converting a large project to TypeScript can sometimes be daunting. It’s clear that the starting point is a bunch of JavaScript, totally lacking static types, and in the end you want to have very specific types across the entire project and all of your dependencies, but jumping straight to that end point in one shot can be very dangerous. In this chapter, we’ll walk through a hands-on example together, demonstrating how a migration like this can be conducted on an enterprise-scale project.
Keep in mind…
- Stay focused on carrying out specific tasks. It’s very easy to succumb to the temptation to do too much at a time. Don’t worry, we’ll get there, and if we stay well-organized, we’ll get there safely and without incident
- Make sure you have a solid test suite. It’s very easy to make a simple change (e.g. changing a
if (foo)
toif (typeof foo === 'undefined')
) which can break your program in subtle ways - Don’t let the perfect be the enemy of the good. TypeScript was built to allow incremental adoption — take advantage of it! The types we’ll be using early on in the process are way less descriptive than what we’ll want to end up with. That’s ok!
An overview of the approach
Each of these steps should involve verifying that building and testing the program works, and a separate git commit
.
- Get TypeScript into your build toolchain, type-checking your existing
.js
files, in the most permissive mode possible - Rename some files from
.js
to.ts
, fixing only the things necessary to get the compile working -
Forbid implicit
any
s, replacing them throughout the codebase with explicitany
s,{}
s or more descriptive types- Improve error handling by enabling
useUnknownInCatchVariables
in tsconfig - Install community-supported types from DefinitelyTyped where necessary
- Improve error handling by enabling
- Start formalizing type information relating to your codebase. Make some
interface
s andtype
aliases. -
Add safety to boolean expressions, and improve handling of
null
andundefined
- Enable the ESLint rule
@typescript-eslint/strict-boolean-expressions
to catch problematic truthy/falsy expressions - Enable the tsconfig option
strictNullChecks
to ensure that ifnull
is desired to be an allowed value in a type, it has to be explicitly stated as such - Enable the tsconfig option
exactOptionalPropertyTypes
to catch occurrences where optional properties are explicitly set to the valueundefined
instead of being deleted
- Enable the ESLint rule
-
Improve types for functions by doing the following
-
In
tsconfig.json
- Add safety around
Function
methodsbind
,call
andapply
by enablingstrictBindCallApply
- Add safety around
this
types by enablingnoImplicitThis
- Catch inappropriate
function
vsfunction
type-checking by enablingstrictFunctionTypes
- Ensure all code branches within a function consistently return a value, or return no value by enabling
noImplicitReturns
- Add safety around
-
-
Improve typing around
class
es-
In
tsconfig.json
- Ensure class fields are initialized by the time instances are returned by constructors, by turning on
strictPropertyInitialization
- Start requiring use of the
override
keyword on methods that override a same-named method on a base class, by enablingnoImplicitOverride
- Ensure class fields are initialized by the time instances are returned by constructors, by turning on
-
-
Start to get rid of explicit
any
s in some places. This is a BIG step, and should be done in smaller increments-
In
.eslintrc.js
- Ensure functions don’t return an
any
orany[]
, by enabling@typescript-eslint/no-unsafe-return
- Ensure values of type
any
aren’t passed to functions functions by enabling@typescript-eslint/no-unsafe-argument
- Ensure values of type
any
can’t be called (as a function) by enabling@typescript-eslint/no-unsafe-calls
- These two rules are going object to a lot more things, relative to the other two in this group. Consider doing these steps in smaller chunks, each with their own commit
- Ensure that member access (grabbing a property) can’t be performed on values of type
any
by enabling@typescript-eslint/no-unsafe-member-access
- Ensure variables can’t be assigned a value of type
any
orany[]
by enabling@typescript-eslint/no-unsafe-assignment
- Ensure functions don’t return an
-
-
Develop a clear distinction between access of known properties and “dictionary access”
- Require that known properties must be accessed via
foo.bar
syntax, by enabling the tsconfig optionnoPropertyAccessFromIndexSignature
- Require that “dictionary access” must be performed via
foo["bar"]
syntax, and represent that any given dictionary value may beundefined
by enabling the tsconfig optionnoUncheckedIndexedAccess
- Require that known properties must be accessed via
-
Require an eslint-disable comment for all remaining explicit
any
s- Turn on the ESLint rule
@typescript-eslint/no-explicit-any
- Turn on the ESLint rule
-
Remove or appropriately denote unused and unnecessary things
- Turn on
noUnusedLocals
andnoUnusedParameters
in tsconfig, to catch unused variables and function parameters, respectively - Turn on
@typescript-eslint/no-unnecessary-type-assertion
to catch places where the use of a type assertion isn’t necessary in order to narrow a type for downstream usage - Turn on
@typescript-eslint/no-unnecessary-type-arguments
to catch places where an explicit typeParam is provided unnecessarily (meaning the same type would have been inferred) - Turn on
@typescript-eslint/no-unnecessary-condition
to catch places where, as long as the types are correct, a condition will always be evaluated as eithertrue
orfalse
- Turn on
@typescript-eslint/no-type-constraint
to catch places where type constraints are written in a way that doesn’t change the allowed values (e.g.T extends any
is justT
)
- Turn on
There are a wide range of TypeScript ESLint rules available, but not all of them are necessarily useful as part of the journey to add static types to a formerly un-typed codebase. In particular, I would avoid making stylistic changes (e.g. naming conventions for certain types of declarations) during this conversion journey, as this is more perturbance of the codebase while it’s in a partially-typed state.
We won’t go through every one of these steps together, as the whole point of this approach is that the work becomes quite methodical and predictable.