Using Flow instead of Typescript in 2023

Any mention of using Flow in the year 2023 causes strange reactions for web developers everywhere. Why would you do that!?

The tradeoffs won’t make sense for everyone, but there are actually some very compelling reasons to choose Flow instead of Typescript.

Let’s start with the things that Typescript is undeniably better at.

Typescript has much better availability of type definitions #

If you look at NPM download metrics alone, Typescript is about 80x more popular than Flow. This comes with some serious benefits. Typescript is ubiquitous and virtually every module on NPM has typescript type definitions available.

If you’re using a type-system to improve your developer experience and get better auto-complete, Typescript is clearly the right choice.

That said, it’s becoming easier than ever to convert Typescript definitions to Flow. Flow supports mostly the same syntax now. You simply have to convert some extends to : and mark somethings as “$ReadOnly` explicitly to get the correct "variance” behaviour.

Typescript also has better editor support #

If you use anything other VSCode, chances are that Typescript likely has good support and Flow support is an afterthought if there is any kind of support at all.

Zed is a new code editor generating a lot of buzz. It has Typescript support built-in, but since there are no third-party extensions yet, there’s no Flow integration at all.

Nova is an editor for macOS by Panic that is a lot like VS Code but made a fully native app with great design. It has a great first-party Typescript extension. There is a Flow extension available, but it only shows type errors after you save a file, and it doesn’t give you any auto-complete at all.

Typescript’s template string literal types are handy #

Typescript has a lot of very interesting features and to many people, it may seem like Flow is missing out. However, Flow has actually adopted most of the powerful features from Typescript, often with the exact same syntax. In recent versions, Flow has added support for “Mapped Types”, “Conditional Types” and “Type Guards”.

Flow gives you much more type safety #

If your goal is to prevent runtime exceptions, Flow protects you much better than Typescript does. Even if we consider Typescript in “strict” mode, it is easy to trick Typescript a lot of the times. The most obvious example of “unsoundness” (the type-system letting you do incorrect stuff) in Typescript is “variance”.

What is variance? Let’s look at a simple example:

class Animal { ... }
class Dog extends Animal { ... }
class Cat extends Animal { ... }

const dogs: Array<Dog> = [new Dog(), new Dog(), new Dog()];
const animals: Array<Animal> = dogs;

Do you see the mistake there? Most people don’t, and nor does Typescript. The mistake here is that an Array<Dog> is NOT an Array<Animal>. But, you would say, every Dog is and Animal. That’s true. But an Array<Dog> is NOT an Array<Animal>, because Arrays are mutable.

Let me show you why it is an error:

animals.push(new Cat());

dogs.forEach(dog => dog.bark());

TypeError: dog.bark is not a function. (In ‘dog.bark()’, ‘dog.bark’ is undefined)

Uh oh, you accidentally put a cat in your list of dogs, and Typescript just let you. Flow would’ve caught the error and saved you from a potential runtime exception.

Let’s see the two ways you can do what you want in Flow:

const readOnlyAnimals: $ReadOnlyArray<Animal> = dogs;
// It's safe if you promise not to mutate this list.

const animals: Array<Animal> = [...dogs];
// It's safe if you make a copy of the array.

Typescript treats all such types of types as “co-variant”. This works work collections such as arrays and objects as long as you don’t do any mutation. Depending on the type of code you write, this is OK, but problems start to arise as Typescript, by default, also treats types that should be “contra-variant” (types flow in the opposite direction as that of the types contained) as “co-variant”. The most common example of this is refs in React. Typescript will actively guide you to do the wrong thing in such cases.

Do you want a type-system to save you from common mistakes, or do you want a type-system that makes the same common mistakes as you so it doesn’t “annoy” you?

Flow has much better object types #

Typescript’s object types are surprisingly bad. Typescript lacks any concept of “Exact Object Types” that have been available in Flow for years now. An issue on Typescript’s repo has been open for 7 years now, and people just want a way to define object types that disallow extra properties. No luck!

Want an object type where you want a specific key to have one value but any other key to be a different value?

type Data = {
  id: number,
  [key: string]: string,
};

Again, Typescript won’t let you do this. People come up with creative workarounds to make this work. Badly. Flow just handles it like a champ!

Further, Typescript expects you to use & to combine object types together, which means if your keys collide you get strange results. Flow lets you use Object Spread syntax instead!

type CombinedObject = {...ObjType1, ...ObjType2};

Once you get used to this, Typescript’s handling of Object types feel archaic by comparison.

Typescript just gives up sometimes #

There are many situations, where you can lie to Typescript, and it will just let you.

function isNumber(x: unknown): x is number {
  return typeof x === 'string';
}

Typescript will just let you do this. Once you say the return type is x is number, Typescript just trusts you implicitly. Flow will double-check.

You can also often just use x as Y somewhere and even if that’s not the case, Typescript will just let you do that.

Again, Typescript is not built to be safe, it’s built to be helpful. Flow is built to catch mistakes.

Flow’s tooling is starting to get better #

For the longest time, tooling to generate Flow type definitions from source code was abysmal. This is starting to change. The flow-api-translator package will let you extract types from your Flow-typed code and publish .js.flow alongside your .js file for NPM package. It even supports generating .d.ts files as well out of the box. This is a different way to ship types in your NPM package, but it works just as well as the usual process of bundling all types for your package into a single file, and generally is more flexible when you want to let users use files within your package.

Now, flow-api-translator is quite new and far from perfect, but it’s improving fast!

It might be time try Flow once again #

Now, using Flow is near as nice an “experience” as using Typescript. You need to do more work up-front to get all the type definitions. Editors don’t work as well. The tooling isn’t as good.

But if you want a solid type-system that actually catches your mistakes and prevents most runtime exceptions, Flow is a great choice that gets you very far without having to switch languages to something like Elm or ReScript.

 
2
Kudos
 
2
Kudos

Now read this

Revisiting “Features CSS Needs” from 2020

Back in 2020, I wrote an article titled “Features CSS Needs” I wanted to revisit it since so much of it has already come true, and to update my list for 2023. A More Powerful calc # Support for dividing units has been part of the... Continue →