DEV Community

nyaomaru
nyaomaru

Posted on

I Tried Reading React's Source Code and Flow Beat Me Up. So Let's Learn 🚀

Hi everyone!

I'm @nyaomaru, a frontend engineer who eats too much cheese 🧀 and is slowly gaining weight.

Quick detour: this article says that to become a good engineer, you need a deep understanding of code, and that reading existing code is essential:

https://dev.to/thebitforge/10-developer-habits-that-separate-good-programmers-from-great-ones-293n

I totally agree.

So I started doing a deep reading of React's source code.

And honestly?

It’s been surprisingly fun.

Every day I discover something new, like:

  • “Oh, memo bails out when there’s no Fiber + props/state update!”
  • “Wait, <Activity> in v19.2 is wrapped with Offscreen and switches behavior by mode?”
  • “They use bit flags instead of booleans… no wonder it’s fast even with complex state!”

If you have some free time during the holidays, I highly recommend trying it.
How about “React deep reading without giving up” for New Year’s Eve?

…just a thought.


But then… Flow happened 😇

As I kept reading, I hit a wall.

I’m used to reading TypeScript, but React’s core is written in Flow, and there are many places where Flow’s type system behaves differently.

Honestly, I almost gave up.

“Wait… is this syntax? a type? a comment?
…a spell?”

My brain felt like it was being beaten up physically.

So I decided to write this article with one goal:

Understand Flow’s quirks quickly, and lower the barrier to reading React’s source code.

This article is for:

  • People who want to read React’s source code more deeply
  • People who’ve heard of Flow, but never really understood it

Alright, let’s dive in!


🌊 What is Flow?

First, let’s clarify what Flow actually is.

https://flow.org/

Flow is a static type checker for JavaScript.

At first glance it feels similar to TypeScript, but the philosophy is different:
Flow is much closer to a pure type system.

Flow has been developed by Meta (formerly Facebook) alongside React itself, so React’s core type definitions are deeply written in Flow.

The key point is this:

Flow adds type safety on top of JavaScript.

It does not extend JavaScript with new language features like TypeScript does.

That difference matters a lot when you read React’s internals.


🧩 Why should we learn Flow?

If you read React’s source code, you’ll constantly encounter Flow types in core concepts like:

  • Suspense
  • Fiber
  • Lanes
  • Offscreen

Especially tricky concepts include:

  • variance (covariant / contravariant / invariant)
  • maybe (?T)
  • mixed / empty
  • exact objects

Many of these aren't the same meanings in TypeScript.

Without Flow knowledge, you’ll often stop and wonder:

“Is this a type? syntax? or some utility?”

That friction adds up and makes reading slower — and easier to give up.

Once you understand Flow, React’s source code suddenly becomes much more readable.


⚔️ Flow vs TypeScript (quick comparison)

Aspect Flow TypeScript
Primary goal Static type checking Static types + language extensions
Strictness Stricter / theory-oriented Practical / sometimes permissive
Notable features Exact Object, variance, opaque types Rich unions, enums
React core Written in Flow Apps mostly written in TypeScript
Learning benefit Understand React internals Best for app development

👉 Flow is NOT “the origin of TypeScript.”

They evolved on different paths, with different goals.


🌀 Flow Types We’ll Focus On

We’ll skip the basics that feel similar to TypeScript, like:

  • Primitive types
  • Literal types
  • Functions
  • Utilities

Instead, we’ll focus on Flow types that often confuse people when reading React:

  • mixed
  • empty
  • maybe
  • opaque
  • exact / inexact objects

(There are many more — check the official docs if you’re curious!)

For variance, see this article:
https://dev.to/nyaomaru/understanding-variance-in-typescript-flow-covariant-contravariant-invariant-bivariant-4fbi


🌪️ Understanding mixed

Let’s start with mixed.

In Flow, mixed is very similar to TypeScript’s unknown.

You’ll see it in React when:

  • handling errors
  • dealing with callbacks
  • accepting “anything, but safely”

mixed is the safest “anything”

From a type theory perspective, mixed is a top type.

It means:

“Any value can be here, but you’re not allowed to use it until you prove what it is.”

Unlike TypeScript’s any, which says “do whatever you want,”

mixed says: “Show me the type first.”

function numberOrString(x: mixed) {
  // x + 1;   // ❌ error
  // x.name; // ❌ error

  if (typeof x === 'number') {
    return x + 1; // ✅ OK
  }
}
Enter fullscreen mode Exit fullscreen mode

You must narrow before using it.

This prevents bugs caused by casually handling unknown values.

If you know unknown in TypeScript, this should feel very familiar.


🕳️ Understanding empty

Next up: empty.
(Don’t worry — not talking about my bank account.)

empty represents the bottom type in Flow.

empty means “this value can never exist”

Literally:

“There is no possible value of this type.”

Not number, not string, not null — nothing.

This is very close to TypeScript’s never.

function foo(x: empty): empty {
  return x; // type-checks, but can never be called
}
Enter fullscreen mode Exit fullscreen mode

This function is logically unreachable.

Flow often uses empty to represent impossible states.

Why is this useful?

Example: unreachable branches.

function bar(x: string) {
  if (typeof x === 'number') {
    return x; // ❌ unreachable → inferred as empty
  }
}
Enter fullscreen mode Exit fullscreen mode

Or functions that never return:

function throwError(message: string): empty {
  throw new Error(message);
}
Enter fullscreen mode Exit fullscreen mode

Or exhaustive checks:

function voice(value: 'cat' | 'dog'): string {
  switch (value) {
    case 'cat':
      return 'meow';
    case 'dog':
      return 'bow';
    default:
      return (value: empty);
  }
}
Enter fullscreen mode Exit fullscreen mode

If you forget a case, Flow will complain — at compile time.


🌫️ Understanding maybe (?T)

Flow’s maybe type is written as ?T.

It means:

T | null | undefined;
Enter fullscreen mode Exit fullscreen mode

Much shorter, right?

function greet(name: ?string) {
  if (name != null) {
    return 'Hello, ' + name.toUpperCase();
  }
  return 'Hello!';
}
Enter fullscreen mode Exit fullscreen mode

Using != null is intentional here — it safely removes both null and undefined.


🕶️ Understanding opaque types

Now for one of Flow’s most stylish features: opaque types.

Normally, if two types share the same structure, they’re interchangeable.

Flow lets you say:

“They look the same — but they are NOT the same.”

TypeScript alias example

type UserId = string;
type PostId = string;

const user: UserId = 'aaa';
const post: PostId = user; // allowed
Enter fullscreen mode Exit fullscreen mode

Aliases are just aliases.

Flow opaque types

opaque type UserId = string;
opaque type PostId = string;
Enter fullscreen mode Exit fullscreen mode

Locally they look similar — but export them, and things change.

export opaque type UserId = string;
Enter fullscreen mode Exit fullscreen mode

Outside the module, UserId becomes truly opaque.

You cannot forge it.

This gives Flow true nominal typing.

TypeScript’s branded types are similar — but can be bypassed with casts.
Flow’s opaque types cannot be forged outside the module.

That’s why they’re so powerful.


🧩 Exact vs Inexact Objects

This is not a personality test.

Flow objects come in two flavors:

  • Exact (default) — strict
  • Inexact (...) — permissive

Inexact

type User = {
  name: string,
  ...
};

const user: User = {
  name: 'nyaomaru',
  age: 123, // OK
};
Enter fullscreen mode Exit fullscreen mode

Extra properties are allowed.

Exact (default)

type User = {
  name: string,
};

const user: User = {
  name: 'nyaomaru',
  age: 123, // ❌ error
};
Enter fullscreen mode Exit fullscreen mode

Even passing through variables won’t bypass this.

This strictness prevents accidental object pollution — something TypeScript allows in some cases.


🏁 Final Thoughts

Once you understand:

  • mixed
  • empty
  • maybe
  • opaque
  • exact / inexact

React’s source code stops looking like magic spells.

You start thinking:

“Ah — that’s why they typed it this way.”

So grab some noodles (or pasta 🍝), and try reading React’s source code again.

Happy holidays, and happy hacking!


Bonus

Please check my OSS for building type guards easily: is-kit 😸

https://github.com/nyaomaru/is-kit

Top comments (0)