Blogged Answers: Learning and Using TypeScript as an App Dev and a Library Maintainer

This is a post in the Blogged Answers series.


Thoughts on my experiences learning and using TypeScript, from different perspectives

Intro

2019 has been the first year where I really learned and began to understand TypeScript. I feel that my experience as both a Redux maintainer and an application developer has given me some potentially interesting insights and opinions on learning and using TypeScript, and I'd like to share those thoughts. Hopefully these will be useful and informative to folks.

Table of Contents

Background: My Programming Language Experience History

To provide some context for how I began using TS, let's rewind the clock all the way back to the beginning, and I'll tell the tale of how I got to my current situation.

First Steps

I didn't actually start programming until my freshman year of college. (I'd checked out "Learn C++ in 21 Days" from the library a couple times while I was in high school, but always returned it unopened.)

My first programming class was procedural-oriented C++ (basically C-level functions, but C++ cout for output). At the end of that class, I thought I knew so much about programming.

Freshman year me was clueless :)

That summer, I wanted to write my first GUI program: an app that could take a webcomic site URL, a comic image filename pattern, and a date range, and download all comics in that range. This of course required that I find some kind of C++ GUI toolkit, and after some research, I settled on Qt. Qt is a huge, powerful, complex, object-oriented GUI widget framework, that requires a custom C++ build toolchain and relies on deep class inheritance hierarchies and use of pointers. I had experience with none of those. But, I kept poking around the docs, and somehow figured out enough to make it work.

My second programming class, that fall? "Object-Oriented Programming with C++". I did fairly well in that class :)

I learned the basics of Java that year, picked up Python on the side, and then got to use Python at an internship. Amusingly, when I got back to school and had to write C++ again, I had completely forgotten how to write #include statements and a main() function - Python had gotten me used to just throwing a bunch of statements in a .py file and running them.

After my junior year, I ended up writing an HTML editor app for the Windows Mobile 5.0 / PocketPC platform. I had seen little bits of the Win32 API that base-level Windows programming required, and had no interest in working at that low an abstraction. So, I built it using C# and the .NET Compact Framework. This turned out to be a major challenge, as the .NET CF 1.0 was horribly limited compared to the desktop .NET Framework. I managed to find enough workarounds to build something useful, and after publishing it on some freeware/shareware sites, actually had real users using it and sending me suggestions. (Or, in some cases, bug reports! I got my first crash report less than 24 hours after publishing the first version of PocketHTML. Turns out that hardcoding a path of "\Program Files\PocketHTML\someconfig.ini" fails if it's a non-English device :) )

My senior project was a C++ IDE designed for freshman C++ programming students, which would allow the professor to dynamically enable more advanced features over time. We built this in C++, and I used the wxWidgets GUI toolkit to build the UI.

So, by the time I graduated college, I was pretty comfortable with statically-typed programming languages, and concepts like generics. I'd also done enough Python to like the idea of dynamic languages as well.

Learning JavaScript in the Real World

I never touched web development until a few years after I'd gotten a full-time job as a software engineer. My first web app, which I'll refer to as "AppK", had a requirement that the backend be written in Java. During the design process, someone introduced me to a technology called Google Web Toolkit (GWT), a Java-to-JS compiler framework, and a third-party UI widget framework called SmartGWT. Those tools allowed me to use my existing desktop GUI framework development experience and Java background to build what was really an early Single-Page Application, but without needing any "real" web knowledge at all.

Well, mostly. You see, I needed to use some other pieces that needed to be integrated, like a jQuery-based datetime picker, the Google Earth browser plugin, and the Cesium.js 3D globe library. All of those needed to be wrapped up in GWT's "JSNI" syntax, which lets you write Java function signatures using the native keyword and a special comment delimiter, and then the body of the function is actual JavaScript that gets included in the output. I kept having to add these interop layers so that my Java code could work with the JavaScript, and while most of what I did was pretty hacky, it was just enough to do what I needed. (Story of my career, really!)

In 2013, I was switched to a new team building a ground-up rewrite of an existing app, which we'll call "AppB". Unlike AppK, AppB was just JS from day 1. Sadly, it was also just jQuery from day 1. Suffice it to say we didn't have much real web dev experience on our team, and very quickly wrote ourselves a bunch of spaghetti code, reinvented wheels, and technical debt.

I'd been reading about this new concept of "JS MVC frameworks" that were becoming widespread. While we didn't have time to investigate them due to a tight initial dev schedule, we found time later in the year to compare Angular, Ember, and Backbone. I pushed us to adopt Backbone, due to the ability to convert pieces of our jQuery spaghetti over to Backbone piece-by-piece. I ended up doing the majority of that conversion myself.

By 2014, I had cobbled together a bunch of Backbone plugins to add more functionality, like Marionette for lifecycle methods, Epoxy for data binding, and Ampersand for better models. As we began adding new features, I started teaching teammates how to use all this Backbone/MVC stuff. Along the way, I began to feel more confident in how JS worked, including things like the nuances of this. I also wrote a whole bunch of Python for our backend services as well.

By mid-2015, I'd finally truly grasped the concept of having data drive your UI. Meanwhile, the AppK GWT codebase I'd built was still sitting around, and felt pretty clunky after all this easy-to-modify JS code I'd been writing. Recompiling took multiple minutes, so the editing cycle was pretty slow. (Also.. SINGLETONS! SINGLETONS EVERYWHERE! whimpers).

I began tossing around the idea of rewriting AppK using some kind of modern JS toolset. I'd heard about this "React" thing, and began trying to learn it in my free time. Also, there were these things called "Flux" libraries that seemed to go with React, and most of them had weird names that were mostly Back to the Future puns, like "McFly" and "Marty". Oh, and there was some new one called "Redux" that had just come out, and people seemed to be excited about it. Also, there was some set of chat channels called "Reactiflux" that had a bunch of folks hanging out and discussing React-ish stuff.

Over the course of the next year, I went from learning React and Redux, to answering questions in Reactiflux and Stack Overflow, to writing the Redux FAQ, to being handed the keys as an actual maintainer of Redux.

In the process, I had become fluent in modern ES6. I still knew how to write code in statically-typed languages, but between writing JS on the front end and Python on the back, I found myself rarely actually writing any typed code at all.

Slow Steps into Typed JavaScript

Initial Awareness and First Experiences

I'd seen the initial announcement of TypeScript when it first came out, and Flow kept getting mentioned as part of the React ecosystem, so I was aware they existed. I even had added a "Static Typing" page in my React/Redux links list, so I'd read a bunch of articles on the topic.

Still, I was pretty happy with writing untyped JS. After all, it had worked well for me this far, right? I mean, I'd written a very large portion of AppB's JS client codebase, and I could keep enough of the codebase in my head to get stuff done. Certainly no changes needed there.

But, I was curious how these "static types for JS" might work out in practice. By mid 2017 we'd mostly completed converting AppK's client to a brand new React+Redux codebase, so I spent a couple weeks experimenting with adding Flow to see what was needed to set it up and how much benefit it could give. I specifically chose Flow at the time because it allowed me to mark just a few files as needed to be type-checked, and the fact that it was a linting-type tool made it easy to run the checks separately. I was able to get some key files annotated and get some improved autocompletion in WebStorm, so I counted the experiment a success and moved on.

In early 2018, a new subproject spun off from my main project at work ("ProjectF"). As the project was getting started, the team lead asked me and another relatively-JS-expert dev if we thought the new codebase should use TS, Flow, or no typing. Both of us said it should actually use TypeScript from day 1, rather than starting with plain JS and possibly converting later. Looking back on my advice, I think by this time it had become obvious that TS was winning the marketshare war against Flow, so I was willing to suggest it.

I actually set up the client codebase for ProjectF. CRA didn't have any TS support at the time, so I used the "CRA Typescript" fork of CRA. I found the experience of that CRA/TS starter to be incredibly frustrating! This was largely due to the heavy-handed and obnoxious linting config, which not only had a ton of style-related lint rules, but also treated all lint rules as compile errors (!). I eventually had to resort to disabling the linting setup entirely just to get anything to work right.

A few days later, Ryan Florence complained about the CRA/TS starter on Twitter, and I chimed in with my own frustrations. Some guy named Shawn Wang pointed me to CRA/TS issue #329: TSLint errors cause compile failure, and I left a comment complaining about the restrictive lint setup. (Shortly after that, Dan Abramov saw the discussion and opened up an additional issue to suggest changing the lint rules).

Based on all that, my first hands-on experiences with Flow and TS were... mixed. I understood the sales pitches, what the tools were for, and how to use them, but the developer experience and setup process seemed to be kind of lacking.

Maintenance Annoyances

During this whole time, I'd been seeing an increase in the number of issues filed against the Redux repos that concerned TypeScript. Frankly, I deliberately flat-out ignored those. I knew I didn't have any actual TS experience yet, and therefore didn't have any meaningful feedback to offer.

Given that the Redux core wasn't seeing any active development, there weren't many "real" issues being filed. That meant that the percentage of TS-related issues seemed to keep growing.

This started to annoy me as a maintainer, for a few reasons. I'm used to being able to answer questions and provide help, and I couldn't do that with TS issues. I also couldn't just close them, because I didn't know if they were resolved or not. And, for that matter, a lot of those issues seemed to be about very nitpicky typing-related topics, and since the real code is just fine, why should I be worrying about this other stuff?

Beyond the issues, it seemed as though many folks in the TS community were, frankly, zealots about typing. It seemed to parallel the stereotypical reputation of folks who were into Functional Programming, where everything had to be absolutely 100% perfectly "correct", or it was just flat-out wrong. In general, the attitudes of a lot of TS fanatics really rubbed me the wrong way.

Finally, the incredible complexity of these types bothered me. JS is a highly dynamic language, and the Redux libraries take full advantage of that dynamic behavior. Trying to capture all that dynamic behavior in static types resulted in type declarations that ranged from complicated to basically unreadable, like this lovely little snippet from the React-Redux types:

    <TStateProps = {}, TDispatchProps = {}, TOwnProps = {}>(
        mapStateToProps: null | undefined,
        mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps, TOwnProps>,
        mergeProps: null | undefined,
        options: Options<{}, TStateProps, TOwnProps>
    ): InferableComponentEnhancerWithProps<
        ResolveThunks<TDispatchProps>,
        TOwnProps
    >;

By mid-2018, I was frustrated enough that I started asking on Twitter for feedback on possibly dropping the TS typings from the Redux core repo and moving them to DefinitelyTyped, and linking to examples of the numerous TS-related issue threads I was annoyed at. (Daniel Rosenwasser from the TS team actually chimed in and agreed that might be a good idea.)

Gaining Practical Experience

Doing It Live

In late 2018, I got back in touch with the ProjectF team, who were using TS because I'd told their lead they should, and were using the codebase I'd set up for them.

Or perhaps "using TS" is the wrong way to say it. When I looked at their codebase, it looked like every single type in the codebase was just any. I'd pushed for them to use TS on the grounds that it would improve long-term maintainability, but clearly their usage wasn't helping anyone at all. The ProjectF team was mostly a bunch of Java and C++ devs that were doing web dev for the first time, and had run into a brick wall of a learning curve trying to understand JS+React+Redux+TS simultaneously. And really, the use of any was understandable. The TS compiler kept yelling at them with red squigglies and compile errors, and they just wanted to "make stuff work" as fast as possible. Unfortunately, this gave them almost all of the overhead of declaring types, with none of the benefits. Truly, the worst of both worlds.

Given my experience with React, a couple of the ProjectF devs asked me to show them how I'd refactor one of their components. We grabbed a conference room, and they showed me a component that did some data fetching and rendered a bunch of inputs to show a fairly complex data object.

I glanced over the code, and said "Uhhhh... first thing I'm going to do is try writing a TS declaration for this data type. Do you have an example of the API response"? They pulled up a sample, and I started typing away. "interface MyDataObject, field name: string;, field size: number, ...". From there, I tried declaring interface MyComponentProps {item: MyDataObject}, applied that, put the cursor in the render method, typed this.props.item.,... and the Intellisense kicked in and showed the rest of the item's fields. From there I tried extracting a function component, adding some types for its props, and threaded the props types on down, and showed that you could get compile errors if you pass an invalid prop. The other two devs had their jaws kinda drop - wait, you mean there is a point to this TS stuff after all? (Paraphrasing their thoughts, but basically their reaction.)

The impromptu demo was so helpful for them that they asked me to repeat it for the rest of the ProjectF team a couple days later. I went through the exact same exercise with one of their other components, showing how to declare an interface for an API response and add types for component props.

This was, as far as I can remember, my first real hands-on experience with writing TS code. Amusingly, this actually kind of mirrored my first experience with React and Redux, where I'd spent a bunch of time answering questions about how to use them before I ever had a chance to write my first real working app code.

Getting Hands-On

In January 2019, I somehow had a couple weeks free at work, and chose to start experimenting with converting my team's AppB over to TS.

AppB was about 30% React+Redux at this point, but the rest was still Backbone, with doses of jQuery plugins mixed in. I'd converted it over to use Webpack and Babel a year earlier.

I began working on updating the Webpack+Babel config to enable TS support. Fortunately, CRA had just added TS support in v2.1.0, so I was able to dig through their implementation and see how they'd done it.

Once I had the TS build support in place, I began converting a few of our existing files to TS to see how the process would work. I started with a fairly simple file that had some math utils (which was easy - everything's a number!), then progressed to converting some of our React components and eventually some files full of API calls and other logic. I only converted a few files due to time limits, but got enough done to prove that large portions of our existing codebase could be converted. I then had to set the research aside and move on to other tasks.

Converting Redux Starter Kit

At about this same time, some users of Redux Starter Kit had filed PRs to convert it to TS, which I'd agreed to. We released the TS conversion as RSK 0.4.

Shortly thereafter I wrote a long comment laying out My Vision for Redux Starter Kit. In that comment, I said:

Reasonable TypeScript Support

I've recently become aware that apparently more people are using Redux with TypeScript than I had previously thought. Per a recent Twitter poll I ran, looks like it's around 40% or so.

I want to support people using TypeScript, same as I want to support anyone else using Redux in specific ways. So, RSK should be reasonably well typed.

At the same time, I don't want "perfect typing" to become the enemy of "good functionality". A majority of Redux users still use plain JS, and I don't want to get bogged down writing mile-long arcane type signatures or playing "whack-a-squiggly" when I know the code works right as-is. I'm fine with trying to shape things so they work well in TS, but I'm very willing to ship a feature that has less-than-ideal typing if that's what it takes to get it out the door. (Plus, I still barely know TS anyway, and while I hope to get more TS experience in the very near future, I need to be able to work on this lib myself.)

I spent the next few months focusing on the React-Redux v7 rewrite, and then shepherding the v7.1 hooks API. By the time I got back to working with RSK in the summer, I'd found myself becoming frustrated with the results of the conversion. I no longer felt qualified to actually maintain most of the converted code, again due to the highly complex typings. I kept having to ask for volunteers to add new features, and beg several RSK users who were TS experts for help when TS issues were filed. After one of those PRs came up, I tweeted:

Ah, the joy of reviewing a PR for a lib you maintain, when the PR contains code that you are not even remotely qualified to pass judgment on...

type CaseReducerActions<CR extends SliceCaseReducers<any, any>> = {
  [T in keyof CR]: CR[T] extends (state: any) => any
    ? PayloadActionCreator<void>
    : (CR[T] extends (state: any, action: PayloadAction<infer P>) => any
        ? PayloadActionCreator<P>
        : CR[T] extends { prepare: PrepareAction<infer P> }
        ? PayloadActionCreator<P, string, CR[T]['prepare']>
        : PayloadActionCreator<void>)
}

I knew that converting RSK to TS was the right move overall, but I wasn't happy with how the results were turning out for me as a maintainer.

Real World Usage

Greenfield Development

In spring 2019, my project at work got the green light to finish converting our team's AppB completely to React, and also rewrite another GWT app from scratch ("AppL"). We also re-org'd from an "app team" structure to a "feature team" structure. As part of that, I transitioned from being "just" a feature dev on my team (albeit kind of the unofficial tech lead), to co-leading a "UI and Services Infrastructure" team. Our responsibilities included picking the tools for the other teams to use, teaching everyone how to use those tools, owning any "common" code note owned by the feature teams, and working with the feature teams to ensure everyone was doing things the right way.

After confirming that we'd be using React and Redux for both projects, I made two executive decisions:

  • We were going to use TypeScript from day 1
  • We were going to use Redux Starter Kit for all the Redux logic

I worked with my co-lead (the former lead dev on the AppL team), and we set up a brand new CRA-TS project. Since this was the real CRA, we didn't have any of those ridiculous lint rules to worry about this time. I walked him through some of the basic patterns we'd need to use, including use of RSK's createSlice(), and some of the other devs began copying those approaches as they started prototyping features.

I did get a bit of pushback from a couple of other devs on my insistence that we use TS. One of my teammates was specifically concerned about the additional learning curve for other folks, and he had some valid reasons to be concerned.

The AppL devs were almost all Java and C++ folks, and due to various internal constraints, we were also going to have to have some other Java and C++ folks start doing front-end work as well. Since the ProjectF team had hit such a hard learning curve, I was determined to keep that from happening again. I put together a giant "JS for Java Devs" presentation to help get them up to speed on JS, its syntax and nuances, and the JS ecosystem. I also added a short section at the end introducing TypeScript. I also put together an updated and focused version of my React/Redux links list, trying to focus on key concepts they'd need for learning JS, React, Redux, and TS.

At the same time, I also spent a few evenings working on a small standalone prototype app in my free time to experiment with using TS, the new React-Redux hooks API, and Emotion, to get an idea how these things might work out in practice.

As we started looking at completing the conversion of our AppB, we decided that trying to migrate the rest of the Backbone and jQuery piece-by-piece would be too painful. Instead, we set up another new CRA-TS project, and began prototyping that app from scratch as well. In the process, we were able to start sharing some noticeable amounts of code between the two codebases.

My Head Asploded

I spent most of my time building out some fundamental parts of the code infrastructure for both projects. As part of that, I built a more advanced version of the Redux-driven dialogs technique I'd used on AppK, since both apps are built around a desktop-like multi-window desktop-style UI.

I'd previously noted a middleware called redux-promising-modals, which allowed Redux-driven dialogs to have "return values" by returning a promise from dispatch() when a dialog was shown, and resolving it when the dialog was closed. I built my own version of that approach as part of my dialogs implementation here.

However, this introduced a types problem. By default, dispatch() returns whatever you pass in (ie, the action that was dispatched). Other middleware can alter this. Most commonly, the redux-thunk middleware returns whatever your thunk function returns.

The JS version of the code worked great. The problem was trying to convince the TS compiler that, when this one specific showDialog() action was dispatched, it would actually return a promise instead of the action object.

I begged for help on Twitter, and got a solution from the community that worked. I don't even pretend to understand the type declaration, but here it is:

type MyStore<
  O extends ConfigureStoreOptions<any, any>
> = O extends ConfigureStoreOptions<infer S, infer A>
  ? {
      dispatch: {
        (action: MySpecialAction): Promise<string>;
      };
    } & EnhancedStore<S, A>
  : never;

const storeOptions = {
  reducer: rootReducer,
  middleware: [...getDefaultMiddleware(), dialogPromiseMiddleware]
};

const store: MyStore<typeof storeOptions> = configureStore(storeOptions);

Note: After publishing this blog post, Lenz Weber came up with a much simpler way to implement this same behavior:

const _store = configureStore({
  reducer: rootReducer,
  middleware: [...getDefaultMiddleware(), dialogPromiseMiddleware]
});

const store = _store as ({ dispatch: {
    (action: MySpecialAction): Promise<string>;
} } & typeof _store);

I was able to confirm that this approach works in the real app, and have updated the app code accordingly.

Connecting the Dots

In early August, I ran into a problem trying to define types for connected components that use thunks. Specifically, this pattern breaks:

const mapState = (state: RootState) => {}
const mapDispatch = {someThunk};

type MyCompProps = ReturnType<mapState> & typeof mapDispatch & PropsFromParent;

This is because the thunk's real type is () => (dispatch, getState) => void. However, from the component's point of view, it's just () => void. React-Redux has an internal type that "resolves" the thunk by skipping the (dispatch, getState) part. That meant my attempt to use the object shorthand and do typeof mapDispatch broke the types.

I'd done a bunch of bookmarking of TS-related articles over the previous year. While digging back through them for ideas, I came across a gist labeled "ConnectedProps - the missing TS helper for Redux". It suggested reusing another React-Redux internal type to infer and extract "the type of the props passed down by connect". I pasted that snippet into our codebase and updated our internal docs to tell everyone to use this approach.

Porting Redux to TS

About the same time, I was having some conversations in Reactiflux about how the different Redux libs handle TS. The Redux core is written in JS and ships its own types; React-Redux is written in JS and the types live in DefinitelyTyped; and RSK is written in TS.

Nick McCurdy filed an issue suggesting we either move the Redux core types to DefinitelyTyped, or rewrite the lib in TS. I commented "realistically, Redux is not going to be ported to TS any time soon". But, just for kicks, I asked fellow maintainer Tim Dorr "do we ever foresee the rest of the Redux libs getting rewritten in TS?".

Tim expressed concerns about readability and said he was against a rewrite, but said he might be interested in seeing a parallel implementation of Redux in TS for comparison. He put up a Twitter poll asking about a rewrite, and a full 65% of respondents said we should convert it.

A few folks immediately expressed interest in doing the conversion and jumped in. In less than a month, we went from talking about dropping our types entirely to merging a PR that finished converting the entire Redux codebase to TS.

The effort has stalled a bit since then, as we need to figure out if there's any improvements that can be made to the types at this point, but when we do finally release Redux 5.0 it will all be written in TS.

Diving Deep into Conditional Types

As I pushed Redux Starter Kit towards 1.0, one of the last features I wanted to get in was a suggestion to include the individual case reducers in the createSlice return object. I decided it was time I tried tackling one of these changes myself, and threw together a PR to add the case reducers in the slice result.

I pinged the usual list of TS-expert RSK contributors for review. Lenz Weber (@phryneas) pointed out several major flaws in my implementation, and suggested some type tests that would help show that another approach actually worked right. Over the next few days, he continued to coach me through understanding what the types needed to do, explained how conditional types work in TS, and pushed me in the right direction when I got stuck. I was extremely thankful, and said "part of me just wants someone to fix this, but at the same time I really need to start becoming not-totally-incompetent with this complex type stuff.". In the end, I had a correctly working PR, and I finally started to understand that a lot of this "conditional types" stuff was really just about writing the equivalent of comparisons and ternary statements to figure out what the right type should be.

Feeling Comfortable

To swing this all back around to where I began: as of the last few months I've found myself answering a number of TS usage questions in Reactiflux. Certainly not any of the deeper and more complex questions, but I've chimed in on quite a few that were at least more than just basic syntax.

In addition, I've been doing code reviews on all our client code for the work app rewrites, and I've done a lot of pointing folks in the right direction with TS usage as part of that.

(As a related note: the transition of our Java/C++ folks to a TS+React+Redux stack seems to have gone fairly well. Most folks have adapted, and while I've had to correct a number of copy-pasted mistakes and some of the usual misunderstandings, overall it looks like the teaching efforts have paid off.)

Finally, I was able to follow up on that ConnectedProps<T> concept I'd found in a gist. Turns out there was an open issue on DefinitelyTyped to add this to the React-Redux types. I commented asking if this could be moved along, and then found out a PR had been merged in already!. From there, I pushed through getting a new "Static Typing" page for the React-Redux docs, which officially documents and recommends the ConnectedProps<T> pattern

I don't think I'll ever be a complete TS expert, but I've at least hit a point where I'm very comfortable with the syntax and most of the concepts you'd use on a daily basis.

Lessons, Opinions, and Takeaways

So here we are, 4500 words later, and I haven't yet actually gotten around to giving my actual opinions on all this stuff. (If you've stuck around this long, congratulations!) So, I'll offer up some thoughts to consider.

Thoughts as an App Developer

TypeScript is a Requirement

This one's pretty simple. I am completely sold on using TypeScript for application development, and don't want to ever write plain JS app code ever again.

I'll explain why as I keep going.

TypeScript Has Tradeoffs

In my "JS for Java Devs" slides, I listed several pros and cons of using TypeScript:

  • Pros 👍:
    • Documentation: static types tell devs what variables look like quickly - especially valuable when working with unfamiliar code
    • Compile-time errors: common issues like typos or undefined values can be caught immediately, rather than at runtime; compiler prevents passing invalid values
    • Intellisense: type declarations allow IDEs to provide proper autocompletion and type information when writing code
    • Refactoring: can confidently rename / delete / extract code, rather than searching and "hope I found all the uses"
    • Long-term maintainability: better codebase information for future developers who may rotate on and off the project
    • Code Quality: doesn't replace unit tests, but can help minimize errors
    • Library Support: most common 3rd-party libs either ship typings, or community has created their own
  • Cons 👎:
    • Learning Curve: additional syntax and concepts take time to understand, on top of knowing plain JS by itself
    • Time to Write Code: literally more code to write out than just plain JS
    • Difficulties Typing Dynamic JS: can be difficult to come up with good static types for highly dynamic JS behavior
    • Inconsistent/Missing Library Types: not all libs have typings, and quality can vary
    • Compilation Time: TS usage can slow down build times
    • Over-Emphasis on Type Coverage: some TS users spend too much time trying to achieve "100% perfect static type coverage" of an entire codebase, leading to bizarrely complex types

So yes, using TypeScript does mean you have to write literally more code. However, to me, the net benefits in terms of long-term codebase maintainability and catching errors at compile-time is absolutely worth it!.

TypeScript Acts as Documentation

In late 2018, a co-worker who mostly does back-end needed to add a new feature to AppB's JS UI. I knew what parts of the code needed to be touched, and did a screen-share to walk him through that.

Specifically, we had a function that took an options object, assembled an AJAX request body, made the AJAX call to create an item, and then did some additional updates based on the response.

I was very familiar with that function. I had written that function. Problem is, I'd written it about 5 years ago. I knew it took an options object as the parameter, and that there were a bunch of optional fields.. but I couldn't remember what fields were in the object, or what their types were.

I had to read through that function several times and reverse-engineer what the fields were by trying to identify all the uses of the options object, and in a couple cases backtrack to the call sites to figure out what the potential types of those fields were.

If the code had been written in TypeScript already, I would never have been confused about what the function arguments were. Sure, having any kind of documentation for the function would have helped here (JSDoc, simple comments, etc), and I only have myself to blame for not documenting that. But, having the code be in TS would have made it obvious what the types were.

TypeScript Prevents Many Errors

I have some examples to back this one up (yes, more stories!).

We've been porting some of the plain JS code from the old version of AppB to the new version, and we haven't yet gotten around to converting that ported JS to TS yet.

While a co-worker was working on porting a particular feature, there were several cases where he ran into runtime bugs. In each case, the JS code was interacting with the TS portions of the codebase. The TS code was correctly typed, but the plain JS code was able to call it with incorrect values because that side wasn't being type-checked. So, all of those mistakes would have been prevented if we'd just gone ahead and converted the code to TS right away.

Also, while working with other devs on various new features in those two greenfield app rewrites, I've seen TS catch numerous errors at compile time. Those errors never got committed, because we caught them early.

On top of that, I recently filed PRs to update projects like react-boilerplate to use Redux Toolkit. As part of that, I switched to the store setup logic to use RTK's configureStore API.

However, the project codebases were in plain JS, and in the process, I screwed up. In both PRs, I wrote configureStore({reducer: rootReducer, initialState}).

That's wrong. It's preloadedState, not initialState.

I wrote that API, and I used it wrong.

The only reason I finally realized this was one of react-boilerplate's tests kept failing after trying to pass in an initial state value, and asserting that a component had the right output.

Oops.

If I can't remember my own API, think of all the other mistakes people might make!

I don't think that TypeScript replaces tests, but it should give you a baseline level of safety and lessen the need for a lot of things you would have written unit tests for otherwise.

I Want Well-Typed Libraries

As an application developer writing my app logic in TS, I also want all of my libraries to be well-typed. This is particularly ironic, given how much I was whining about people filing types-related issues on the Redux repos. In fact, just a couple days after posting a set of whiny tweets, I ran into a problem with RSK's own types and had to file a types issue against my own library!.

So, I'm now at a point where I will refuse to use libraries that aren't well-typed, and that is a major point of consideration that I'll use when evaluating libraries as possibilities.

Thoughts as a Library Maintainer

Correctly Typing Dynamic Libraries is Difficult

JavaScript is an incredibly dynamic language. You can modify any object at any time, call functions with a different number of arguments than they expect, randomly smoosh objects together, and so much more. Trying to perfectly capture that dynamic behavior in static types is almost impossible.

The Redux libraries in particular heavily rely on dynamic JS behavior, whether it be dispatching an arbitrary action with an arbitrary payload, complex overloaded arguments to connect, or trying to combine multiple sources of props to pass down to your own component.

Unfortunately, this means that our types can require extreme expertise to read, understand, and maintain. As someone who is still not a true TS expert, this is still a source of frustration for me. I love that we have a community that's willing to work on this for us, but I need to be able to understand and work on this code myself as well.

That leads me to my next thought.

TypeScript Pushes You Towards Simpler APIs

Because typing complex / dynamic / overloaded APIs is so hard, TS really pushes you to write simpler APIs that are easier to type.

React-Redux is a perfect example of this. connect, as a higher-order component, is hard to type as both a library author and a user. It has four arguments, all optional. mapDispatch can be either a function or an object full of functions. And, it passes down props that are combined from three separate sources: mapState, mapDispatch, and the parent component.

On the flip side, our new React-Redux hooks API is incredibly simple to work with. useSelector just requires that you declare the type of the state argument in your selector, and the return value usually gets inferred. useDispatch literally just gives you dispatch, and it's up to you to call it yourself. Those are vastly easier to use from a types perspective.

Dealing with Types Maintenance is Difficult

It's still frustrating to know that the actual JS code is "correct", and yet folks are filing issues about the types that feel like they're nitpicking things.

Also, it almost throws the idea of semantic versioning out the window. If a new TS release or a small tweak to your library types could cause someone else's build to break, does that mean you should actually be releasing nothing but major versions?

The only thing I can figure is that most types stuff should get classified as "patch" fixes unless it's truly obviously a major deliberate breaking change.

Thoughts as a Teacher and User

Unclear Where Learning TypeScript Fits In

I was lucky, in that I was already fully familiar with static type systems before I even began learning JS. When I started actually trying to learn TS, it was easy to see how a lot of the TS type syntax worked, and I already knew how to use things like generics.

For many other developers who might have only worked with languages like JS, Ruby, and Python, static type systems are a completely unfamiliar thing, and that will make learning TS much more difficult. For new developers, it's a ton of additional overhead on top of the flood of other concepts and terms they're having to learn.

I don't know what the right teaching sequence is for introducing TypeScript to folks, especially if they're just learning.

There Are Multiple Levels of TypeScript Understanding

I'd say there's at least 3 meaningful levels of understanding TypeScript:

  1. Application Understanding: comfortable with writing TS on a daily basis; can write interfaces for API response and component props, function arguments, and maybe the occasional ReturnType or other bit of type manipulation
  2. Intermediate Types Understanding: able to manipulate types to accomplish goals, like use of Partial, Omit, accessing indexed fields, and extracting types.
  3. Type System Expert: Able to explain why and how the type system works, and come up with new and unique type system manipulations to accomplish difficult tasks.

If I had to guess, I'd say I'm somewhere around a 1.8 out of 3 on that scale atm.

It's Now Easier to Try Using TypeScript

The addition of TS support to CRA makes it really easy to set up a TS project in seconds - it's just create-react-app my-app --typescript, and go.

Similarly, the availability of online IDEs like CodeSandbox and StackBlitz means that people can set up a new TS project with just a couple button clicks.

On top of that, VS Code's TS support is first-class (which is natural given that it's from Microsoft, and I believe the VS Code and TS teams work with each other). I've traditionally been a WebStorm user, and still much prefer it for a number of reasons, but VS Code's ability to display inferred TS types has gotten me to switch over for the most part.

Pragmatism is Vital

As I've mentioned a few times, some of the comments and attitudes from the TS community have frustrated me. It feels like a number of TS users would rather spend hours trying to come up with unreadable and incredibly complex type declarations, rather than solving real problems and shipping code that does something useful.

I'm a pragmatist. I'm all in favor of doing things "right" as much as possible, but ultimately my goal is to build the stuff that our app needs, make sure it meets requirements, and move on to the next task.

The point of writing code isn't to play "how can we create magic incantations with the type system?". Yes, I understand the purpose of having "type safety" throughout the codebase, and that code that isn't typed or has uses of any introduces holes into that safety net. However, as a developer there's a good chance that I know that this code path is safe and correct - it's just that it would take a long time to figure out how to convince the compiler that's the case.

My goal is the "80% sweet spot" of type coverage. I'd like types for our API calls, Redux state and logic, and React components. As long as those are basically in place, I'd say we're in fairly good shape, especially given that our team is new to TS. If there's a bit of code that is particularly difficult to type, I'm entirely happy to slap in a type FixTypeLater = any, and move on with my life instead of banging my head against a wall for several hours.

Note that I'm not saying I'm against having 100% type coverage. It's just that, like unit test coverage, getting that last 10-20% becomes much much harder, and it doesn't seem like the benefits are necessarily worth the amount of time you have to put in to make it happen.

The flip side of this is that using plain JS is a reasonable choice in some situations. If you're writing a smaller app by yourself, or it's a throwaway prototype, TS probably isn't going to provide as much net benefit. I personally would probably still use TS for those cases, but don't feel like you have to use TS for every single thing.

Final Thoughts

Well, as usual, that one got a bit longer than I expected. Hopefully this extended recap of my own backstory, learning process, and experiences has proven useful and informative.

Overall, I think that TS should be used in any meaningfully-sized app that will be maintained long-term, or worked on by multiple people. But, at the same time, TypeScript is a tool, with strengths, weaknesses, and tradeoffs. Take time to understand those tradeoffs and whether they are a benefit for you, and use it wisely.

I'm interested in hearing feedback on your own experiences learning and using TypeScript. Please feel free to leave a comment below, or ping me as @acemarke on Twitter and in the Reactiflux chat channels on Discord.


This is a post in the Blogged Answers series. Other posts in this series:


Author Avatar

Mark Erikson

Collector of interesting links, answerer of questions