1. 27

I am thinking about having my engineering team adopt Typescript. We have multiple large JS codebases, the biggest of which is about 2/3 Coffeescript, Backbone, and Handlebars, and 1/3 ES6 with React. We use Node, too - it’s not all front-end JS.

We have quite a lot of bugs, we have to do a lot of hotfixes, and I believe that a number of these could be mitigated by using Typescript. We rarely refactor code, and I believe that it’s largely due to it being slow and difficult, and easy to break.

I don’t think anyone on the team has any previous experience with Typescript. I expect a number of them have not worked with a statically-typed language before.

What have you learned from adopting Typescript? I would love to hear tips, tricks, or ideas for how to make this change - whether it’s management or process level (I am an engineer and manager) or code-level.

  1. 12

    This has been nothing but a net win for our team, and we have a similar setup as you, with CoffeeScript + Backbone + React -> And now TypeScript. Adding TS support was also painless thanks to @babel/preset-typescript. Then TSC is only used as a type-checker.

    If you want a peak at our codebase, see: https://github.com/artsy/force. (These days it’s mostly used as a shell for components / apps imported from https://github.com/artsy/reaction, though. Reaction is 100% TypeScript.)

    As far as typings go – this is important – unless you have someone holding folks’ hands and helping them through, i wouldn’t turn on strict TS settings. When things aren’t strict it’s basically just JS and then everything is additive. As your team learns more and becomes more familiar with TS, more types will be added and things will get stricter on their own, organically. For entirely new codebases, it’s very worth it to start with strict settings. For old code-bases, migrations, or teams who need to onboard, not worth it.

    Don’t be afraid of any if you can’t figure something out! Use any to keep the flow of things going smoothly, with // FIXME: any attached. Momentum is important. Keep the momentum! Then later, go back and fix the any’s. If you can’t figure it out now, that’s ok too – your codebase is already safer just being in the type-checking pipeline in many other ways.

    For typing react children, there’s React.ReactNode, JSX.Element, React.FC and so on. It can get as strict or as loose as you’d like it to get, but typically ReactNode and JSX.Element will cover things.

    Go slowly, you’ll learn more with time. At Artsy we had a few (two) people who were familiar with TS, and the rest not. We’d never go back to untyped code now, even though in the beginning it was a little confusing at times. Absolutely, without a doubt, it was worth it.

    1. 2

      Thanks! How did you deal with the CoffeeScript, if at all? I assume the Typescript compiler doesn’t know what to do with it?

      I think my team is pretty over the CoffeeScript, anyway - I think they’d all rather be writing ES6. I thought maybe we should compile the Coffee to JS and commit it, then delete the Coffee, just as a way to get to an all-JS codebase that Typescript can understand.

      1. 3

        How did you deal with the CoffeeScript, if at all

        Everything is piped through webpack and babel, and things all exist side-by-side: https://github.com/artsy/force/blob/master/webpack/envs/baseConfig.js#L21-L23 https://github.com/artsy/force/blob/master/webpack/envs/baseConfig.js#L38-L49 https://github.com/artsy/force/blob/master/.babelrc#L13

        For files we find we touch a lot, like lower level lib code, we’ve converted that from Coffee to TS, but 99% of the CS we’ve written is still CS, and we tend to not touch it – its just maintenance code at this point.

        As mentioned below, rather than getting bogged down worrying about your large code base, it’s best to setup TypeScript and then use it to write new code, and then – if need be – convert older, more critical parts later via something like https://decaffeinate-project.org/.

        1. 1

          Thanks, I’ll look into that. Out of curiosity, have you ever thought about just committing the compiled Coffee, so that you don’t need to support it anymore? I have increasing numbers of engineers who don’t know Coffeescript (even though it’s just a different syntax) and don’t want to learn it, but we do unfortunately have quite a lot of coffee that still needs updating. If I’m going to ask them to learn one thing it’ll be TS, not Coffee, which is why I’ve wondered about just getting rid of it…

          1. 2

            No, we have the assumption that if need be people will have to work with it, which is a bit annoying but also not the worst thing. It’s still a pretty nice lang all said, just not type-checked :)

            1. 1

              Yeah, I mean… it’s not that hard IMO to learn CoffeeScript. I don’t personally think it’s a big ask for people on my team to deal with it occasionally.

      2. 1

        I just want to say that I love Artsy and your guys’ work.

        1. 2

          Thank you!

      3. 8

        One thing you can do immediately without changing any code is add a tsconfig file to the project and tell it to check the JS files too, then pull in all the @types/ libraries into the devDependencies.

        Then you can start including small parts of the project in your tsconfig, because if you feed it the whole thing at once it will most likely find a TON of errors and false positives, which is counterproductive and discouraging.

        After you fix the low-hanging fruit you’ll likely be left with a bunch of false positives which you would only be able to fix by adding type annotations. Now, you could just rename File.js -> File.ts, and if you can get away with it easily, i.e. if you’re using Babel >=v7 which has native support for TS, by all means do it, but maybe you can’t and integrating TS into the build process will take more time than you/your team is willing to spend on this experiment and will otherwise just discourage you from trying.

        What you can do in this case is sneak in type annotations right in the JS files inside JSDoc comments, and when things get too hairy add a .d.ts file (which doesn’t have to be required to use the types defined in it, unlike .ts files). Typescript can read those comments just as if they were annotations in TS files. You can get far with this approach, and when you’ll eventually make the switch to TS you’ll just have to transcribe the JSDoc annotations to proper TS syntax. Or you can just keep using the JSDoc-annotated JS files if you think that’s too big an effort.

        Edit: this page has lots of documentation on JSDoc annotations.

        1. 4

          We’ve found that using checkJs: true can be hugely discouraging in a large codebase migration. Best (IMO) to keep a clear division between old and new.

          1. 4

            Yes, that’s why I mentioned including just a small part, by using files or include in the tsconfig file. That should keep it more manageable.

            1. 1

              Thanks for the ideas! It sounds like maybe before we start it would be helpful to have an upgraded build pipeline? Latest babel, etc…

              1. 2

                That would definitely help a lot, @babel/preset-typescript makes it trivial to use TS. But you might have trouble with this (I recently had to upgrade webpack and babel on a Backbone + TS/React codebase and it caused a lot of pain) while adding a tsconfig and JSDoc annotations is a low effort and low commitment process that you can start doing right away to see some benefits.

        2. 4

          Instead of introducing Typescript, another approach could be to add e2e tests first to your existing codebase using Cypress. We already had unit tests, but since 2017 started adding e2e tests which has increased stability greatly.

          We have a 6+ year old monster app still in active development, which has different types of stacks working together: Backbone, Marionette, React, PHP (FuelPHP and Laravel), Python (Django). Cypress doesn’t care what tech stack you use, and we’ve lately have been much more confident in deploying new features.

          If you like to know more or need help with some setup, let me know.

          1. 1

            How did you know our old Coffeescript code also doesn’t have any unit tests? ;)

            I hadn’t heard of Cypress - I’ll have to check it out. We seem to have a reasonable amount of automated testing (I think through Browserstack?) but it’s owned and run by the QA team, so it’s not helpful to a developer as they’re working. It might take a week or more for that QA process to find a bug and send the ticket back :(

          2. 4

            Finally, a lobste.rs conversation I can meaningfully contribute to! I spent a lot of time converting CoffeeScript to TypeScript on my employer’s codebase — something like 600 .coffee classes at peak :)

            As @sibeliuss mentioned elsewhere, decaffeinate is pretty great for getting rid of the CoffeeScript. We’re actively using it to get rid of our remaining CoffeeScript, with a few extra steps after to add rough TypeScript typings. If you’re not ready to make that jump (or there aren’t any tests to validate that nothing has regressed from the decaffeination process), there are still options!

            When I started our migration (back when TypeScript 2.1 was new), I really wanted to use --no-implicit-any, since that’s where many of our bugs came from at the time. We took a two-pronged approach:

            1. For slow-moving, core modules (network queues, logging facades, etc.), hand-write .d.ts files. You’ll get huge benefits in your typescript without having to worry about regressions.
            2. For modules that change often, I wrote a tool that converts the CoffeeScript compiler’s AST to a TypeScript .d.ts file. Function names and argument names come over correctly, but all paramters are optional and any (but explicit any, so new TypeScript gets strict typing from the start!). Module exports do get exposed properly though, and it’s been running pretty smoothly for us for about two years.

            We’re doing more conversion (mentioned above) to finally get rid of the old stuff. Now that --checkJs is a possibility in TypeScript, decaffeinate definitely seems worth your time!

            That tool is available on Github if you think it’d help at all. A few of our conventions got encoded into the translation (mostly around method visibility), but it’s otherwise a straightforward translation. There will eventually be a corporate blog post about that whenever I get around to editing it :)

            1. 1

              Ohh, splitshot is interesting, thanks!

              1. 1

                Wow, splitshot seems really cool! I will definitely take a look at that. I bet it was an interesting project to work on. I would love to see the blog post about it when it’s ready - you should post it to Lobsters :)

                (and also if you would PM it to me or put it in a reply to this message so I don’t miss it, that’d be really helpful)

              2. 2

                I haven’t adapted an old codebase to use TypeScript, but have used it from the start. You should be aware that it adds another layer of dependencies on an already complicated dependency story and the issues it can lead to.

                We ran into issues of compile times and version incompatibility; that if I were to upgrade a library, immutable.js to be specific, from 3.8 to 4.0 the compile times increased from an already slow 2s to 10s. After having worked in ClojureScript with Figwheel, which allows the developer to mutate the program while it’s running without destroying its state, working with such a slow feedback loop was viscerally painful.

                Another thing you have to be prepared for is to write your own type definitions for the libraries you depend on and to correct existing ones. Sometimes you won’t be able to express the extremely dynamic ways some JavaScript behaves, but that’s okay.

                I have no idea about how working with TypeScript contrasts to working with plain JavaScript, I’ve not worked on a large scale JS project recently enough. But I know that it’s a world of hurt compared to ClojureScript.

                1. 1

                  Another thing you have to be prepared for is to write your own type definitions for the libraries you depend on and to correct existing ones.

                  This is one of my biggest concerns. We’ll be on a learning curve writing definitions just for the code we’ve built and know inside out, and I’d be worried about trying to do that for external libraries. I’m hoping we won’t run into too many cases where that’s necessary.

                  We also make heavy use of lodash (who doesn’t?) but it seems like there are types for that, hopefully it works for us.

                  1. 2

                    You can always just start out having it typed any and then refine it from there. The worst part really is when a type declaration is preventing you from using a library the way it’s meant to be used. Objectively it’s no worse than there being no type declarations really, it just feels more frustrating.

                    But honestly it’s no biggie. It will just force you and your team to actually learn the type system and not just flounder around. I wouldn’t be worried about that up front cost; unless you’re afraid that your team will be demoralized by it, in which case you should spend some time thinking about how to prevent that.

                2. 2

                  Here is a post about a team who introduced gradual typing with TypeScript into their codebase: https://go.tiny.cloud/blog/benefits-of-gradual-strong-typing-in-javascript/

                  Here’s a post from a junior developer who converted a JavaScript codebase into ReasonML: https://blog.usejournal.com/journey-with-reasonml-as-a-junior-developer-17ee53a25fa7

                  1. 1

                    Do you people use both eslint and tslint or only the latter?

                    1. 2

                      My understanding is that tslint is being deprecated in favour of ESLint this year. So I guess unless it’s really valuable we’ll probably not adopt it if we switch to TS.

                      Would be interesting to hear what others are doing?

                      1. 2

                        Wrote a blog post about exploring the move from from tslint to eslint: https://artsy.github.io/blog/2019/01/29/from-tslint-to-eslint if yr interested.

                        We haven’t made the full switch yet but it seems promising.