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,
memobails out when there’s noFiber + props/stateupdate!” - “Wait,
<Activity>inv19.2is wrapped withOffscreenand 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.
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:
SuspenseFiberLanesOffscreen
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:
mixedemptymaybeopaque-
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
}
}
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
}
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
}
}
Or functions that never return:
function throwError(message: string): empty {
throw new Error(message);
}
Or exhaustive checks:
function voice(value: 'cat' | 'dog'): string {
switch (value) {
case 'cat':
return 'meow';
case 'dog':
return 'bow';
default:
return (value: empty);
}
}
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;
Much shorter, right?
function greet(name: ?string) {
if (name != null) {
return 'Hello, ' + name.toUpperCase();
}
return 'Hello!';
}
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
Aliases are just aliases.
Flow opaque types
opaque type UserId = string;
opaque type PostId = string;
Locally they look similar — but export them, and things change.
export opaque type UserId = string;
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
};
Extra properties are allowed.
Exact (default)
type User = {
name: string,
};
const user: User = {
name: 'nyaomaru',
age: 123, // ❌ error
};
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:
mixedemptymaybeopaque-
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
You can try to use flow in below playground 🚀
https://github.com/nyaomaru/flow-playground
Please check my OSS for building type guards easily: is-kit 😸

Top comments (1)
This is interesting.