# Type system of the React compiler

## Memo the props #

In React, a component wrapped in React.memo only re-renders if its props change.

```
const Greeting = memo(function Greeting({ user }) {
return (
<h1>
Hello, {user.firstName} {user.lastName}!
</h1>
);
});
```

`Greeting`

re-renders whenever its prop, `user`

*changes*. React uses shallow comparison to determine if the props have changed.

In JavaScript, objects must maintain their identity to make the shallow comparison work, which is why memoization can be crucial. On the other hand, primitive values don't have any identity associated with them and can simply be compared directly.

```
Object.is({}, {}); // false
Object.is(3, 3); // true
```

Consider a simple component that calculates a total based on props:

```
function Price({ items, state }) {
const subTotal = calculateSubTotal(items);
const tax = calculateTax(subTotal, state);
const total = subTotal + tax;
return <Text text={total} />;
}
```

One naive way to prevent `Text`

from re-rendering unnecessarily would be memoize everything like this:

```
function Price({ items, state }) {
const subTotal = useMemo(() => calculateSubTotal(items), [items]);
const tax = useMemo(() => calculateTax(subTotal, state), [subTotal, state]);
const total = useMemo(() => subTotal + tax, [subTotal, tax]);
return <Text text={total} />;
}
```

However, we don't really need to memoize primitives for shallow comparison. The memoization here is wasteful from both a memory and a bundle size perspective.

Can we teach the React compiler that these are primitive values? The React compiler could perform whole program analysis by compiling *all* the files, including the files containing `calculateSubTotal`

and `calculateTax`

to understand that they return a number. But, single file analysis has a bunch of really nice advantages like better performance, incremental rollout and lower compiler complexity.

Can the compiler *infer* that these are numbers from the way they are used?

## Type inference #

One of the most classical type systems is the Hindley Milner type system that's typically used in functional languages. The type inference in the React compiler is inspired by the Algorithm W of this type system, but it's much simpler because JavaScript is too dynamic for sound typing. I'll briefly describe the various steps as implemented by the React compiler.

### Initializing the type variables #

During the initial lowering from JavaScript source to the compiler's intermediate representation, every identifier gets an associated `Type`

variable to store the type. A `Type`

variable is just like any other variable but instead of storing values, we store types in them.

```
type Type = { kind: "type"; id: number } | { kind: "Primitive" };
// { kind: "Type", id: number } represents a type variable
// { kind: "Primitive" } represents a primitive type
let total; // identifier: { name: 'total', type: { kind: "Type", id: 0 } }
```

`{ kind: "Type", id: 0 }`

is the Type variable associated with the identifier `total`

.

### Generating the type equations #

Rather than specifying the type system in scary and not so accessible formal notation, I'll try to explain one of the typing rules using our earlier example.

`const total = subTotal + tax;`

The above statement can be typed by saying that the operands of a `BinaryExpression`

with an `Arithmetic`

operator are primitives and the resulting value is also a primitive. In our example, `subTotal`

and `tax`

are the operands, the operator is `+`

and the return value is `total`

.

```
const total = subTotal + tax;
// subTotal -> primitive
// tax -> primitive
// total -> primitive
```

The first step of the type inference pass is to generate type equations based on the typing rules defined by the compiler. A type equation is simply a statement of equality between two types, like a mathematical equation. A simple type equation can be of the form "`left = right`

", where `left`

and `right`

are types.

In code, it can be as simple as an object with two fields representing the left hand side and right hand side of the equation, like this:

```
type TypeEquation = {
left: Type;
right: Type;
};
```

And concretely, the typing rule we defined above can be generated like this:

```
function* generateTypeEquationsForBinaryExpression(instruction) {
const { operands, lvalue } = instruction;
yield { left: operands[0].type, right: { kind: "Primitive" } };
// subTotal -> primitive
yield { left: operands[1].type, right: { kind: "Primitive" } };
// tax -> primitive
yield { left: { lvalue.type }, right: { kind: "Primitive" } };
// total -> primitive
}
```

Similarly type equations can be generated for other constructs in JavaScript like function application (ie, function call), and if-statements.

### Solving the equations #

The process of solving these type equations is called unification. The unification process attempts to find a substitution of type variables that makes all the type equations true.

Solving the type equations for our example are pretty straightforward. The type variables for `subTotal`

, `tax`

and `total`

can directly be substituted by the primitive type.

But consider the earlier statement that defines and initializes `subTotal`

:

`const subTotal = calculateSubTotal(items);`

At the point of defining `subTotal`

, we don't know its type. Only after we look
at the usage of `subTotal`

have we inferred that this is a primitive.

But in this type inference, the types flow back to the definition as well. We go back to the definition and notice that the return type of the function `calculateSubTotal`

must be the same type as `subTotal`

. And by solving this, we've now inferred that the return type of `calculateSubTotal`

must be a primitive.

This is an example of how incredibly powerful type inference can be! We've inferred the return type of a function that exists in a separate compilation unit without having looked at it's implementation. Type systems often use this inference to boostrap and quickly start expanding inference across an untyped codebase.

But it does come with a significant downside -- if the inference is incorrect, it results in a surprising “action-at-a-distance” behaviors. This is why Flow moved to local type inference, trading more explicit type annotations for better errors.

## (Don't) Memo the props #

Now, going back to our original example of the `Price`

component, the compiler can
infer that all the values are primitives. There's actually no need to memoize
`total`

, `subTotal`

or `tax`

in this `Price`

component, saving bundle size and
memory!

```
function Price(t0) {
const $ = useMemoCache(2);
const { items, state } = t0;
const subTotal = calculateSubTotal(items);
const tax = calculateTax(subTotal, state);
const total = subTotal + tax;
let t1;
if ($[0] !== total) {
t1 = <Text text={total} />;
$[0] = total;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
```

## Typing React #

Once we had the type system, it quickly became clear that we could use it as a platform to do various other analyses too.

Adding validations for certain rules of react became as easy as adding a few typing rules to our type system. For example, rather than building separate validations for each React API, just adding the typing rules for `useState`

hook gives us this validation:

```
1 | const [state, setState] = useState({ foo: { bar: {} } });
2 | const foo = state.foo;
> 3 | foo.bar = 1;
| ^^^ InvalidReact: Mutating a value returned from 'useState()',
which should not be mutated. Use the setter function to
update instead.
```

Note how even interior mutability is caught here -- not just simply a modification to `state`

, but a modification to `state.foo`

through an aliased variable (`foo`

).

## Further reading #

If you're curious to learn more about type systems, the original Hindley Milner type system papers and the more recent Local Type Inference paper are great places to start.

If you're curious to read more about compiler theory in the React compiler, take a look at the other tagged posts.

- Previous: Compiler Theory and Reactivity