· Jake Worth

Migrate to TypeScript by typing a seam first


Migrating to TypeScript is less about the technical details and more about picking a place to start. A technique that I’ve seen work well is to pick a seam.

In many apps, this seam is utility code. Consider this JavaScript codebase:

src/utils/strings.js
export const fullName = (first, last) => [first, last].join(' ');
src/App.jsx
import {fullName} from './src/utils';
export const App = () => {
return (
<>
<p>Hello {fullName('Jake', 'Worth')}!</p>
<p>Hello {fullName('Josh')}!</p>
</>
);
};

This code is broken because fullName expects two names, but our second call only sends one. These are the kinds of problems that type-safety can catch.

Let’s see how. First, we convert our utility file to TypeScript.

src/utils/strings.ts
export const fullName = (first, last) => [first, last].join(' ');

Without doing anything else, your text editor should start complaining in that file:

Parameter 'first' implicitly has an 'any' type,
but a better type may be inferred from usage. (tsserver 7044)
Parameter 'last' implicitly has an 'any' type,
but a better type may be inferred from usage. (tsserver 7044)

TypeScript doesn’t like that first and last are not explicitly typed. So, we type them as strings.

src/utils/strings.ts
export const fullName = (first: string, last: string) =>
[first, last].join(' ');

Inferred types are smart enough to know that this function must return a string.

If this was all we did, we’ve added value, because fullName is better documented. And, if we call it in this file without passing both arguments, we’d get a type error.

The next step is to convert the caller to TypeScript TSX:

src/App.tsx
import {fullName} from './src/utils';
export const App = () => {
return (
<>
<p>Hello {fullName('Jake', 'Worth')}!</p>
<p>Hello {fullName('Josh')}!</p>
</>
);
};

Hovering on the second call is the real payoff:

Expected 2 arguments, but got 1.
Related information:
* strings.ts#1,41: An argument for 'last' was not provided.
(tsserver 2554)

This bug is especially tricky because it isn’t a JavaScript runtime exception. The malformed “Josh” construction doesn’t break, but it’s incorrect.

> fullName("Josh")
'Josh '

This is not a “full name” (the function is a lie) and contains an extra space. That space shows up in the HTML with an extra space between “Josh” and “!”.

<p>Hello Josh !</p>

TypeScript can catch this; JavaScript reasonably can’t.

This technique might not work when you have widely reused utilities. And it might not be efficient enough when you have bugs based on production data that you hope to address immediately with TypeScript.

It’s one approach that has helped me move past “Analysis Paralysis” and start getting TypeScript code into the codebase. Keep adding seams and expanding from your seams until you have a comfortable amount of type-safety.