Incrementally Migrating JavaScript to TypeScript

Now possible with TypeScript 1.8

Types are a Good Idea. Computers are relentless at finding errors, humans not so much. You can get away without using types, but large projects and teams will run better with them.

TypeScript is a mature project to bring types to JavaScript. Originally TypeScript was pitched as a distinct language (like CoffeeScript), but it has since committed to being a superset of standard JavaScript.

Which sounds great, right? You already know JavaScript, you just need to learn the extra type syntax. But how do you actually start adding TypeScript to an existing (and probably large) JavaScript project? Read on to see how TypeScript 1.8 adds a key feature that makes it much easier.

How TypeScript Works — for JavaScript Developers

Coming from the React ecosystem, figuring out how TypeScript integrated into a project was unintuitive. I tried to fit it into my existing model of tooling, but it didn’t quite work. Here’s how I think of it now:

TypeScript is an ES6 transpiler, type-checker, and module transformer. It can be used in place of Babel, Flow, and Browserify.

Out of the box, TypeScript is more on the monolith end of the spectrum and wants to be most of your toolchain — but you can configure it otherwise, which helps us integrate into an existing project.

The TypeScript compiler, tsc, will process all of the files in a directory and spit out their compiled JavaScript variants to a destination directory (or a single bundle file). It’s unlike Webpack and Browserify, which generally take a single file as an argument and emit another file. Like other tools, TypeScript’s compiler can be configured via command line arguments or a tsconfig.json file.

Historically TypeScript only worked with .ts and .tsx files, which made it tough to integrate with .js code. But TypeScript is moving to improve the JavaScript development experience (read up on #4793 and #4789). An early step is for the TypeScript tooling to play nicely with .js files, which is shipping with TypeScript 1.8.

Migration Strategy

Let’s say we want to add some TypeScript to an existing React app, and our app is currently built with Webpack and Babel.

What we’re going to do is run all of our project’s files through the TypeScript compiler, and then change our Webpack config to read from that compiled output. So instead of Webpacking our JavaScript source code directly, we’ll be Webpacking the Typescript output. (wait — is Webpacking a verb?)

This isn’t the only way to migrate your code, the whole subject is rapidly evolving, but it may spark some ideas. Let’s walk through the mechanics of how this happens.

Example

We’re only going to TypeScript-ify code unrelated to React. TypeScript is very handy for React component definitions (you get compile-time checks to ensure your props and state are valid), but it adds a bit more tooling that’s beyond the scope of this article (external type definitions). One of the benefits of moving your app over piecemeal like this is you can avoid adding such tooling complexity until you’re ready.

Here’s our pre-existing toy app (you can find the source on Github):

Our Webpack config:

Finally, our build script looks like:

To add TypeScript, we install it via NPM:

$ npm install typescript@1.8.0 --save

TypeScript’s compiler configuration lives in a file called tsconfig.json; we create that and configure it like so:

allowJs is the option newly available in 1.8. The TypeScript compiler will run a quick sanity check on .js files for syntax errors but otherwise passes them straight through to the output directory. The “es6” options mean that TypeScript will emit ES2015-compatible modules and code, instead of transforming them.

Note that we specified a “tsDist” output folder (ourDir). We need to tell Webpack to read from this folder, not the original “src” version:

Accordingly, our “build” script needs to change:

The folder naming conventions here aren’t necessarily a recommendation for what to do on your project, it’s very much up to you.

Cool, time to actually write some TypeScript. TypeScript will only allow TypeScript features in files with .ts or .tsx extensions; if we try to add typings to a .js file, TypeScript won’t run any of its typing logic and will throw all sorts of syntax errors (recall that allowJs tells the compiler to essentially pass-through JavaScript, it does not enforce any new logic). We begin by renaming our User file:

$ mv src/User.js src/User.ts

When we build, we have a problem:

$ npm run build

…

src/User.ts(3,10): error TS2339: Property ‘handle’ does not exist on type ‘User’.

It’s alive! One of TypeScript’s checks is that all properties of this must be declared. We can fix that pretty quickly:

We can now build safely:

$ npm run build … Hash: 923de2cd435a90a150ce

Version: webpack 1.12.13

Time: 2260ms

Asset Size Chunks Chunk Names

out.js 679 kB 0 [emitted] main

+ 160 hidden modules

Congratulations! This is a pretty trivial example, but you can imagine how it would benefit a larger project. We can gradually increase the percent of code covered by TypeScript and enjoy compounding returns.

Beyond

Before TypeScript 1.8 it was a pain to migrate a codebase, but now there’s a first-class path forward.

It’s a bummer that the JavaScript community doesn’t have a consensus on typing tools. The React ecosystem is invested in Flow, but in my experience the community and usage of TypeScript is much more active. Microsoft is implementing deep IDE support, Angular has adopted TypeScript, and my own Palantir is a heavy user of TypeScript (check out Plottable and TSLint).

I think the TypeScript team has the right idea to make the JavaScript migration experience better with features like allowJs. TypeScript is iterating incredibly fast, and I’m excited to see what happens over the next year.