Learn how to Do a TypeScript Conversion — Sympolymathesy, by Chris Krycho
Assumed audience:
Software program builders working with JavaScript and TypeScript, or occupied with and dealing with gradual sort programs in different languages. In notably: I’m not arguing for TypeScript or Python varieties
or Ruby’s Sorbet and so on.; I’m speaking to people who find themselves already all in favour of adopting them.
Epistemic status:
I led the conversion of a 150,000-line-of-code app to strictly-typed TypeScript again in 2017–2018, and was the first “subject material professional” for LinkedIn’s adoption of TypeScript throughout its tens of millions of traces of library and software JavaScript.
Some of the widespread questions I get from folks all in favour of changing their JavaScript purposes to TypeScript is: How ought to I strategy this? There are two approaches folks have a tendency to consider:
-
A comparatively relaxed strategy: setting
compilerOptions.strict: false
initially, changing information as you contact them, and steadily rising the robustness of the kinds by enabling particular person strictness flags till you’ve all of them turned on — or some mixture of those. -
A extra rigorous strategy: setting
compilerOptions.strict: true
, and really rigorously changing the codebase in a “leaves-first” order, the place no module is transformed with out first having varieties for all of its dependencies. Making specific what “extra rigorous” most likely already implies: that is my most popular strategy.
Most builders (myself included, the primary time I did this!) are very a lot tempted to do the “simply convert a file if you contact it, in unfastened mode or with numerous // @ts-expect-error
and any
scattered round” factor. It looks as if the lowest-friction, quickest, best path:
-
That sample normally works with different kinds of migrations.
-
It feels extra tractable, in that you may simply do it “as you go”.
-
It really works fairly effectively for sufficiently-small codebases — it’s excellent for <1,000LOC and fairly good for <10,000LOC.
Accordingly, additionally it is the strategy I see most frequently really useful to folks beginning out on changing a TypeScript codebase.
Sadly…
You’ll encounter two huge issues if you take the extra relaxed, intuitive, much-recommended strategy. On smaller codebases, these issues could not matter all that a lot, however the greater your codebase is, the extra they may harm.
First, you’ll find yourself having to propagate modifications to numerous information over and again and again:
-
Every time you allow one other strictness setting, you will note new sort errors in lots of modules. The largest of those shall be
strictNullChecks
andnoImplicitAny
, however all of the strictness settings will catch issues missed with out them: that’s the reason the settings exist, in any case. These are usually not normally spurious errors, both. Thus, you’ll have to do one other cross “fixing the kinds” for the module every time you allow a brand new strictness setting. -
If you happen to convert a module however haven’t transformed the modules it is determined by, all the kinds from these dependency modules shall be
any
. If you convert these information, you fairly often discover errors in the best way you had been utilizing their APIs. Similar to with strictness settings, this implies you typically find yourself having to “repair the kinds” for different modules every time you make this type of change.
I scare-quoted “repair the kinds” right here as a result of it’s normally “write the kinds and repair the bugs”. As I’ve written before:
…in lots of circumstances the complexity was already current within the code base. The TypeScript conversion didn’t create that complexity: It uncovered it. Actual-world JavaScript code is commonly extremely sophisticated — certainly, intelligent — in ways in which solely turn into apparent once we attempt to specific in varieties the contracts the code already invisibly assumes. Because of this, conversions from JavaScript require advanced varieties way over code written in TypeScript from the beginning. A lot of the complexity is (completely!) implicit in JavaScript, whereas writing out the contracts in TypeScript makes it specific. That allows higher selections: does this specific API really warrant some sophisticated varieties, or ought to we simply maintain it easy? Often: the latter.
Even so, it will possibly really feel like we’re simply fixing TypeScript points again and again, and I feel you will need to acknowledge that.
This type of factor may be fairly demoralizing at a private degree, as you’re employed to “repair varieties”, as a result of you end up hitting the identical items of code again and again. It will also be tough to speak clearly to much less technical crew members, e.g. designers and product house owners who may be fairly fairly confused about why we have to spend time on doing TypeScript issues for this chunk of the codebase once more — “Didn’t we do {that a} month in the past?” Having to clarify that “Sure, we did, however not all the best way” may be irritating.
Second, and possibly even worse, you can not depend on the issues you’ve already transformed really being protected when taking this strategy. They really feel safer than JS types-wise as a result of they’re in TS… however they aren’t, as a result of they’ve numerous // @ts-expect-error
and any
scattered round. It could find yourself being fairly demoralizing and irritating to have errors popping out of your “however we already transformed this!” modules. It additionally undermines a variety of the guarantees we make when justifying the funding to our administration or companions: “I assumed the purpose of TypeScript was to repair these sorts of bugs, so why isn’t it doing that?” As with having to do a number of passes on the identical information, having to reply “Properly, we transformed this to TypeScript, however not all the best way…” is deeply unsatisfying.
Lastly, the issues described right here scale exponentially in problem with the dimensions of the codebase. With 1,000 traces of code, these issues are minor annoyances. With 10,000 traces of code, they’re a little bit of a problem. With 100,000 traces of code, they’re actively demoralizing. With 1,000,000 traces of code… you would possibly simply by no means end. The friction by no means goes away, so it requires fixed effort to maintain it transferring and get it throughout the end line.
The extra rigorous strategy of setting compilerOptions.strict: true
and strolling the dependency graph so as means you by no means should revisit the file due to elevated strictness or newly-well-typed dependencies. What’s extra, there’s a actually huge upside to the expertise if you do a conversion this manner. When all of your dependencies are already strictly typed, each time you change a brand new module it sits on a strong basis. TypeScript itself can do a good bit of the work of including varieties for you through its code fixes, since it will possibly use the data from the downstream APIs you name. For the elements you need to work out by yourself, you continue to should verify all of the methods the module’s APIs are used, however you don’t should verify all of the issues it makes use of: the issue house is lower in half.
The online is a double win: each module you change actuall delivers on sort security, and each module you contact will get simpler as a result of its basis is protected. The method is sort of a flywheel: each little bit of effort you apply hastens the remainder of the method.
This isn’t a free lunch. It usually requires extra self-discipline and extra specific buy-in from stakeholders. As a substitute of “simply convert a file if you contact it (and normally go away it not-fully-converted)” you want to carve out some devoted time to do the work by tackling a pair modules every week or one thing like that. In the end, although, it makes for a significantly better expertise for everybody concerned.