Blog
12 March, 2022#web#JavaScript#TypeScript

My thoughts on "Types as Comments"

Already so many bytes were spent on discussing the TC39 proposal for type annotations ("Types as Comments"), which has received a surge of renewed interest following the TypeScript team's recent annocement that they will be contributing to it. While I lack the experience & expertise necessary to add much substantive value to the relevant discourse, I'd still like to briefly reflect on how I work on JS/TS projects and share some thoughts.

At my job, I am currently tasked to work on two React applications: one I started and written in TS, and the other I inherited and written in JS.

Given the benefits of static type checking, starting a greenfield project in TS was a complete no-brainer for me. However, for the inherited project, I've found that JSDoc annotations mixed with type definitions in .d.ts files can come very close to actually writing TS when it comes to in-editor DX (using VS Code and jsconfig.json).

In this setup, I define types that are either complicated or shared across multiple components in a separate types.d.ts file and use JSDoc's @typedef tag in actual .js or .jsx files to "import" those types. With this, I can type a simple React component in the following way:

// --------------------------
// src/MyComponent/types.d.ts
// --------------------------
type Foo = {
  // some complicated stuff
};

type Bar = {
  // more complicated stuff
};

// -------------------------
// src/MyComponent/index.jsx
// -------------------------
/** @typedef {import('./types').Foo} Foo */
/** @typedef {import('./types').Bar} Bar */

/**
 * @param {Object} props
 * @param {Foo} props.foo
 * @param {Bar} props.bar
 */
function MyComponent({ foo, bar }) {
  // some component stuff
}

Using import() in JSDoc, I can also consume types from NPM packages I use. For instance, I can type a Redux thunk function in the following way:

function fetchSomeResource() {
  /** @param {import('redux').Dispatch} dispatch */
  return function fetchSomeResourceThunk(dispatch) {
    fetch("/api/resource")
      .then((response) => response.json())
      .then((json) => dispatch(json));
  };
}

Yes, JSDoc tends to get quite verbose and working with advanced types is often tricky. But I don't think verbosity in code is inherently evil and working mostly on React apps, I rarely need to reach for advanced stuff. In fact, the peculiar setup for the inherited project led me to decide against full migration to TS, which would add quite a bit of weight to its container image.

With this experience, I've come to the following tentative conclusion: given the current state of JS ecosystem and conventions, bringing "Types as Comments" to the spec looks like a poor trade-off to make. Its key vale proposition is not type safety but "ergonomics" i.e. convenience; there's no runtime type checking anyways.

Here, it's worth noting that "Types as Comments" is explicitly not intending to replace TS:

"This proposal is a balancing act: trying to be as TypeScript compatible as possible while still allowing other type systems, and also not impeding the evolution of JavaScript's syntax too much. We acknowledge that full compatibility is not within scope, but we will strive to maximize compatibility and minimize differences" [emphasis added].

This leads to at least the following two corollaries.

First, if we want all the goodness of TS, we'll still have to rely on essentially the same (though hopefully improved) toolchain we use today. And even if "Types as Comments" changes its goal to seek full TS compatibility, the different paces of the standard JS vs TS as evolving systems will get in its way. In fact, the very fact that TS is an independent project allows it to move fast, respond to user needs quickly, and improve & innovate as a language. And that "moving fast" includes introducing breaking changes when necessary. The JS standard doesn't have that kind of luxury.

Second, we'll continue shipping annotation-free JS code just like we do today since any type annotation, even if valid, brings nothing more than unnecessary weight to the production code. In fact, any non-trivial JS application will need to go through compilation to generate optimized output. Minified, tree-shaked, bundled, transformed (if necessary), and polyfilled (also if necessary). If this is happening, there is no sense in not stripping off the type annotation even if leaving them won't change any behavior.

To be fair, the "Types as Comments" proposal addresses these points.

On one hand, the proposal fully acknowledges the first point ("Should TypeScript be standardized in TC39?"):

TypeScript has continued to evolve very quickly in both syntax and type analyses, to the benefit of users. Tying this evolution to TC39 risks holding that benefit back. [...] The goal here is to enable wider deployment of systems like TypeScript in diverse environments, not obstruct TypeScript's evolution.

On the other hand, the proposal challenges the second point ("Don't all JS developers transpile anyway? Will it really help to remove the type-desugaring step?"):

The JavaScript ecosystem has been slowly moving back to a transpilation-less future. [...] Implementing this proposal means that we can add type systems to this list of "things that don't need transpilation anymore" and bring us closer to a world where transpilation is optional and not a necessity.

In my view, however, the concession on the first point actually undermines the case for "Types as Comments" while the counterargument to the second point is not sound.

The concession undermines the proposal's case because incorporating type annotation to the JS standard, which is inflexible for good reasons ("Don't break the web!"), necessarily limits what the non-standard type systems can do. And, while I don't consider myself as an expert, I don't see static typing for JS is a solved problem based on a simple observation that the non-standard type systems are still evolving with no end in sight. This means that baking our current understanding into the standard may turn out to be a mistake that cannot be reverted in future.

Meanwhile, the existing type systems, ever improving, are less subject to this issue. Sure, they will always involve an additional cost in time and computing resources, but the incredible adoption and growth in the last few years proves that it's a trade-off worth making.

The counterargument is unsound because it does not sufficiently take into account the conventions and trends in the frontend space, which arguably claims the lion's share of building software in JS and has increasingly embraced trans-/com-pilation for optimization & ergonomics. I don't think it's a coincidence that the proposal emphasizes the experience of backend (Node.js) developers when presenting this counterargument.

In addition, the counterargument also largely fails to consider what the existing ecosystem can offer. As I've briefly demonstrated above, it is already possible to enjoy the benefits of static type checking for any moderately complex project via JSDoc comments and types defined in separate files (which could use JSDoc as well if so desired). This already covers the majority of what "Types as Comments" can offer sans extra convenience. Yes, convenience matters, but prioritizing it should be carefully weighed against all the consequences of modifying the JS standard. Lastly, if the project requires the full power and flexibility of TS, not only is "Types as Comments" insufficient but also its slightly different syntax vs TS can turn into a nuisance.

Clearly, I'm not sold. But then again, many engineers who are far more talented and experienced than I are contributing to the proposal, which is still in the early stage and in active development. So maybe I'll find myself changing my mind in not-so-distant future? I shall keep an eye on it. 👀