When I joined Unsplash, I frequently watched as bugs inevitably slipped into the codebase due to human error. Having worked extensively with TypeScript in my prior work and side projects, I understood how static types could help.

As well as helping to prevent bugs, static types provide living documentation which helps to better understand and maintain legacy code. TypeScript also harnesses the static types to provide powerful and reliable refactoring tools, such as renaming symbols and object properties.

Our migration to TypeScript had to be gradual, so team members could learn the new syntax on the job, and so we could start to get a feel of the real benefits before fully committing. Thankfully, adding TypeScript does not have to be a large and daunting task — any JavaScript project, small or large, can easily begin a gradual migration to TypeScript.

One Make Day I decided to attempt the start of our gradual migration. This is how we did it.

Using TypeScript as a type checker for JS files

To add TypeScript to a project, add a tsconfig.json . Use the files , include and exclude options to define your dependency graph. (I prefer to use files to specify the entry points, and let TypeScript trace the dependency graph from there.)

TypeScript allows you to enable type checking for JS files. We went with option of enabling the checkJs compiler option.

By default, TypeScript is very relaxed. TypeScript will infer types where possible. Everything else will be typed as any by default.

It would have been a lot of work to add TypeScript to our compilation pipeline, so we decided to use TypeScript simply as a type checker instead. This is possible by running TypeScript with the noEmit compiler option: run tsc --noEmit to type check your app.

I spent about a day fixing all the errors, either by fixing the code or adding missing type annotations with JSDoc. Most of the errors were bugs we didn’t know about.

We then added type checking as a build step, and everyone in the team enabled TypeScript in their IDEs (or just switched to VSCode ;-)) so they could see the type errors whilst developing.

Gradually add JSDoc

TypeScript allows you to add type annotations without deviating from the JavaScript standard using JSDoc:

/** @type {number} */

const foo = 1

We gradually added JSDoc type annotations to our application. Again, this meant we could start to get a feel for TypeScript and using types in our application but without committing to any changes in our compilation pipeline.

Gradually add third party typings

All modules are implicitly typed as any . We gradually added typings from DefinitelyTyped. As we did so, new type errors emerged, usually pointing to bugs we didn't know about.

Gradually migrating from JS to TypeScript syntax

After awhile of using JSDoc and TypeScript in this way, we wanted the true power of TypeScript syntax to define our types, instead of JSDoc. It took me 2 (hack) days to get TypeScript added to our compilation pipeline, on the server and client.

Adding TypeScript to a compilation pipeline will be made easier with Babel v7, which adds support for TypeScript syntax. Although you may find you don’t need any of Babel’s features after switching to TypeScript, as we did.

At this point, all our files were still .js . We were now free to gradually rename a file to .ts (or .tsx ) and start using TypeScript syntax instead of JSDoc.

const foo: number = 1;

Today, over 60% of our codebase is TypeScript ( .ts / .tsx ). Other team members are eagerly continuing the migration.

Gradually enabling stricter compiler options

Since we started, we’ve enabled some of the stricter compiler options, such as strictNullChecks and noImplicitReturns . Enabling each of these took about 0.5-1 days in order to resolve all the new type errors.