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
Please check my OSS for building type guards easily: is-kit đ¸

Top comments (0)