
Fast and precise type checking for JavaScript (acolyer.org)
98 points by deltux on Nov 8, 2017 | hide | past | favorite | 38 comments

I used both Flow and TypeScript extensively at my last job (coda.io), and through a series of thoughtful discussions and debates, we chose to migrate to TypeScript, even though we had already started using Flow in parts of our codebase.

Microsoft did a really good job putting resources behind TypeScript, making sure tooling and IDE integrations are good, and generally getting a ton of momentum going. TypeScript is fast-moving, with bugs fixed and features added at a high rate, and the quality of discussion on the issue tracker is high. Flow, in comparison, was not evolving as fast.

Flow touts global inference, which I think means better types when you don't necessarily have annotations on module boundaries, but I mostly care about what's possible in a greenfield or well-annotated codebase.

Flow deserves a ton of credit, and Microsoft probably studied it in detail, but I think the advantages are overblown at this point. Take the soundness thing. TypeScript added null-strictness a while ago, and recently they improved function type polymorphism. The reality is that both type systems have limits and you sometimes need to type something imperfectly or use an escape valve ("any"). In most cases, however, TypeScript gives you more sophisticated tools to construct the types you want, so you actually get better types. Maybe TypeScript lacks a theoretical underpinning for soundness, but there's no reason it can't continue to get "more and more sound," with it harder and harder to find good examples of unsoundness.

I only post on HN about once a year. But I want to chime in on this topic.

At my current job we started a clean slate codebase (react/redux/webpack) beginning of 2016 and had everything typechecked by Flow from the start.

We found Flow to be extremely unreliable and cumbersome to use with not much apparent commitment behind it. As soon as Typescript hit 2.0 we converted the entire codebase (fairly easy to do since the syntax and semantics are very similar') and have been super happy with that decision.

I wholly agree on you remark about soundness vs. tooling. Flow tooling sucked, error messages were cryptic and unhelpful and we ran into obvious mistakes all the time that Flow did NOT catch, without being able to figure out why that was. Editor support was lacking and we just never trusted the typechecker.

Bolting on types to Javascript has tradeoffs and Microsoft did an amazing job of making the right tradeoffs.

There is noticeable effort and momentum behind the project that inspires a lot of confidence in the languages future. It has completely changed the way I write Javascript and I can't imagine going back to an untyped world.

That change was not in rock-solid confidence that TS catches all my errors (although it does a remarkably good job at that), but the sheer productivity improvement types enable (autocompletion, project-wide renaming, all that stuff users of Java or C# IDEs have available to them).

I advocated flow for two years, and I still find it interesting as a typesystem. But what finally led me to give up was the core team's lack of community engagement.

It's very difficult get the team's attention to a bug or a question. As a result, they fail to see the common pain points of actual users.

Typing higher order components is one common pain point that still doesn't have a clear solution. So, unless you're avoiding HOCs altogether, you can't really benefit from type annotations in your react codebase (you'll have too many implicit `any`s).

You'll find more of these common use cases that flow doesn't optimise for. They're all over the issues section. Unfortunately, issues on Github generally don't get much attention from the team either, so I'm guessing users sometimes give up on reporting them. This has happened to me quite a few times. I didn't report some bugs because putting together a reduced test case takes time, and that time would be wasted if your issue is not gonna be paid attention to.

What makes matters worse, is that there is no public roadmap. You don't know what the team's priorities are. You don't know when the bugs that affect your work are gonna get fixed, or some feature you need is gonna get implemented. You don't even know if it's on the radar.

Of course, the Flow team is under no obligation to do any of that. They have no obligation to fix bugs for us or publish a roadmap. I'm already grateful that they've given us access to their work and without charging money. However, they do have the responsibility to communicate what their priorities are. If they're positioning Flow as an alternative to TypeScript, which is a well-supported, community-oriented project, then they should state clearly that Flow simply isn't. Call it a research project, or a project focused on a single company's use cases. Don't ask people to bet their own projects on it, when they clearly shouldn't. It'll be a big loss of productivity for them down the line, and your messaging is partly responsible for that.

Yeah I second this. Despite having a superior type system, flow is so behind in both tooling and 3rd party library definitions it's really hard to justify continuing to use it over typescript. It's weird to say, but the unsoundness of typescript doesn't matter much next to all the other advantages.

Besides, I feel like if I really want a strong type system https://reasonml.github.io/ might be the better choice over flow.

Flow/Typescript both of great but writing from start from scratch both flow and typescript slow me down with all the hassle of needing to define the types of parameters and data structures. Typescript being a bit verbose at time too.

When working with another person API then typescript does become a godsend in helping understand what the bloody hell does this callback parameter are required.


Plot twist: While you are cursing at the person who wrote that API, you slowly realize that it was you all along.

Agree, typescript does help with that slow realization that its a problem with your code passing in undefined instead of a object.

Verbosity is quite helpful when doing maintenance on foreign code.

Apologies, offtopic: wait, Coda.io only uncloaked less than a month ago and you've already left??

I mean, I've worked for several companies and left months before they actually launched. Any contractor who works with startups have been through similar situations.

Can't speak for OP's situation though.

Flow capitalizes on its soundness, but its not as useful when the error messages are so cryptic. We tried adopting Flow at a company I worked at, but dealing with gibberish errors and the huge meaningless stack traces was a productivity sink.

We migrated to Typescript. Error messages are more understandable and unlike flow include line numbers and the column. Add huge number of typings, and ts definitely wins.

There are many gripes we've had with typescript too, but at least we didn't spend hours trying to understand error messages.

so was it Angular or just a JS codebase?

It was a React codebase, which is why we were swayed towards Flow initially.

What is soundness, exactly, and how is Typescript unsound?

It seems like a useless academic term that doesn't work so well in the real world, maybe?

Soundness means that if the type system says that a variable has a particular type, then it definitely has that type at runtime. A sound type system is always correct, and an unsound type system might be incorrect in some cases.

Here's an example of unsoundness in TypeScript:


TypeScript incorrectly (but conveniently) says that Array<string> can be assigned to Array<string | number>, and you can exploit that to create an "s" variable that TypeScript thinks is a string but is actually a number.

You can try the same code in Flow and it gives a type error:


So Flow seem to be complaining that number and string are incompatible types for the array elements. I attempted to change your arr.push(3) statement to one that is appending a string, and it gave the same error even though it should not have given an error in that case.

Neither Flow nor TypeScript are correct in this instance. Neither keep track of the actual array element's value, just the general type of the array, which means they actually don't know for sure and do their best guess. So in the Flow example, it complains that the number and string types are incompatible even though it doesn't know that this specific case is incompatible, just the general case. In the TypeScript example, it should keep track of the type of the argument value supplied during the function invocation, not the type for the argument declaration.

In this case I would argue that even though TypeScript is incorrect, it's preferable to Flow because Flow doesn't infer that the types are incompatible from actual usage but theoretical.

Agreed. This is one of the things people hate about type systems is fighting with them saying "I know better".

This example is quite interesting because javascript itself does not provide a way to check the type of an array, unlike a primitive. This is likely due to the fact that an empty array doesn't really have a type for it's items yet.

Here is an example where typescript can infer the type using the `typeof` runtime check but provide compile time checking.


The issue with changing `arr.push(3)` to `arr.push('baz')` is that there's still a type annotation on the function saying `Array<string | number>`. If you get rid of the type annotation or change it to `Array<string>`, flow is ok with it:


Both Flow and TypeScript have good type inference (with Flow's generally being better, I think) and do pretty well with all type annotations removed, but that wasn't shown in my example because I explicitly annotated all types.

Note that if you do want/need to give an explicit type annotation for this sort of thing, Flow provides `$ReadOnlyArray`, where `Array<string>` is assignable to `$ReadOnlyArray<string | number>`:


It sounds like you're arguing that TypeScript is wrong because it's overly-permissive, and Flow is wrong because it's overly-strict, which makes sense. That's probably why people prefer to use the word "sound" to describe Flow rather than "correct". Every sound type system has cases where you can write perfectly correct code that would be rejected by the type system (which is provable because of the halting problem). Opting into a type system always means that you limit the type of code you can write in exchange for better automatic verification.

An issue with $ReadOnlyArray is that if you're doing something that does work on both String and Number types, like using them for some template string, Flow will complain that the types are not compatible even though in this instance they are. This is a bigger issue with both TypeScript and Flow, in that they don't seem to keep enough track of the data in arrays, only the type of the array, to know if the actual types are valid. If TypeScript/Flow kept track of the array elements and their usage, this issue wouldn't occur in the same way.

> Flow doesn't infer

That's because the author told Flow explicitly to expect an array of strings and numbers.

In the previous paragraph I'm more clear about my meaning, which is that the array elements are not tracked properly, meaning that Flow/TypeScript can't determine if the actual usage is valid 100% accurately.

Thanks for the really concise example.

Just as an example, this program type checks in TypeScript but crashes at runtime:

    class Dog {

    class Greyhound extends Dog {
        doGreyhoundThing(): void {
            console.log("I am a greyhound!");

    class Poodle extends Dog {
        doPoodleThing(): void {
            console.log("I am a poodle!");

    function f(g:(Dog) => void) : void {
        let hound: Greyhound = new Greyhound();

    function h(p: Poodle): void {

`f(h);` would be a type error if function types were contravariant in their argument types. TypeScript made the unsound choice to let function types be bivariant in their argument types, which the authors claim is justified for practical reasons. More info here: https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-fun...

They added contravariant functions in 2.6 using the compiler flag --strictFunctionTypes as described in this PR https://github.com/Microsoft/TypeScript/pull/18654

When I was student (1992), a friend noticed exactly the same issue in the eiffel language. They answered also that the incorrect check was more practical.

I'd be much more enthusiastic about using a type system if it was a part of an official ES-next spec. Right now community efforts around static typing are divided between two very similar, but incompatible type systems, similar to the situation we had a few years ago with CommonJS and AMD modules (though the two module systems are much more dissimmilar than Flow and TypeScript). The module debate has been been mostly laid to rest with the introduction of the official ES modules spec (at least from the perspective of the application developer when it comes to module code they actually write), and I'm hoping an official type annotation spec can do the same for the JS static type checking landscape.

Flow has already long-since demonstrated that it's perfectly possible to introduce useful type annotations to JS code without changing any runtime behavior whatsoever, and Flow and TypeScript have mostly converged on similar syntax and semantics when it comes to the type annotations. Given all this, I'd have thought that the standardization process would be pretty far along by now. Maybe someone more familiar with these matters can offer some insight on the seeming lack of progress?

Interestingly, a type system was proposed for JavaScript around 2007 as part of ES4, which was later abandoned [1]. The proposed details in [2] are strikingly similar to what we've ended up with in Flow and TypeScript.

[1] https://en.wikipedia.org/wiki/ECMAScript#4th_Edition_.28aban... [2] https://www.ecma-international.org/activities/Languages/Lang...

See also ActionScript [1] which was the closest real world language to an ES4 implementation, and how much it seems like Typescript/Flow.

[1] https://en.wikipedia.org/wiki/ActionScript

It feels a little bit like if you'd want a linter to be part of an ES spec. As in: I appreciate TypeScript for the compile-time guarantees it gives me.

A good comparison here is the Python approach: Python 3 made the syntax for type annotations a first-class part of the language but only to the point of parsing that syntax and providing an AST, it did not (and at least for now will not) define "compile time" or "run time" meanings for them.

The immediate benefit to adding Typescript/Flow-like annotations to the ES spec would be that you could run Typescript files unchanged (without transpilation) in the browser, even if the Browser didn't do any type checks for you. You could still use TS or Flow to do the type checks in a separate process, but you could potentially drop the type-stripping steps in TS or Babel. The possibility exists that eventually the browser could also start to enforce basic type checks, but the immediate benefit of slightly faster build processes with no type-stripping step shouldn't be overlooked.

That makes sense. I can also imagine not wanting to start too quickly with that though, to learn what such a syntax would have to support from Flow and TypeScript.

The point of having type annotations in the spec is so that library and tooling authors can focus on providing and refining a single set of type annotations that any tool (Flow, TypeScript, anything else that comes up) can check for correctness and perform static analysis (for things like autocomplete, find references, go-to definition, etc) against. Over time, this would vastly improve the quantity and quality of available type definitions compared to what's available for either the Flow or TypeScript camp alone.

And once type annotations are part of the language proper, the possibility opens up for browsers to use type annotations to provide runtime safeguards, optimizations, and reflections, all of which would be invaluable since static checking alone is often not enough to provide correctness guarantees in all cases in JavaScript. When all is said and done, JavaScript is a dynamic language, and the ability to reuse the same type annotations for both static and dynamic checks is the piece that I'm sorely missing from both the Flow and TypeScript ecosystems so far.

I thought this was a new thing, but it turns out it's facebook's flow. https://flow.org

Yes, it would be good to add that to the title

Both flow and typescript are great tools. Typescript has much better tooling and great IDE integration, using typescript is a no-brainer for new projects. Flow's soundness does shine though once your codebase (and your team) becomes very big, as any unsafe cast can be reliably treated as error. I use flow at work, and so far my experience is that unsafe cast in flow always indicates a bad design or actual bug.

I've always wondered if you could let the engine do the type checking or create type definition files for you, maybe an optional v8 flag?

This would be best case scenario so we could get rid of the tedious work that one has to put in to make an older library have type checking.

Typescript has inference for "plain" JS files if you use the `--allowJS` flag.

That inference is also made available more directly in dts-gen [1], which is a tool built around Typescript inferencing that produces decent first pass type definition files for a library.

dts-gen may be something to try the next time you need a definition file. (Personally, I still prefer to start a new definition file by hand, but it's good to have an automated option, even if only to double-check your work.)

[1] https://github.com/Microsoft/dts-gen

I think the-morning-paper is one of the best things on the internet. It's probably the thing I miss a proper blog reader for most.
