Constrained Writes

Restricting writes to unannotated variables

As of Flow v0.186.0, unannotated variables will be inferred to have a precise type by their initializer or initial assignment, and all subsequent assignments to that variable will be constrained by this type. This page shows some examples of how Flow determines what type an unannotated variable is inferred to have.

If you want a variable to have a different type than what Flow infers for it, you can always add a type annotation to the variable’s declaration. That will override everything discussed in this page!

Variables initialized at their declarations

The common case for unannotated variables is very straightforward: when a variable is declared with an initializer that is not the literal null, that variable will from then on have the type of the initializer, and future writes to the variable will be constrained by that type (try flow):

1
2
3
4
5
6
let product = Math.sqrt(x) * y;
// `product` has type `number`
let Component = ({prop}: Props) => { return <>{prop}</> }
// `Component` has type`React.ComponentType<Props>`
let element = <Component {...props} />
// `element` has type `React.Element<React.ComponentType<Props>>`

Any subsequent assignments to product, Component, or element will be checked against the types that Flow infers for the initializers, and if conflicting types are assigned, Flow will signal an error (try flow):

1
2
3
product = "Our new product is..."; // Error
Component = ({other_prop}: OtherProps) => { return <>{other_prop}</> }; // Error
element = <OtherComponent {...other_props} />; // Error

If you want these examples to typecheck, and for Flow to realize that different kinds of values can be written to these variables, you must add a type annotation reflecting this more general type to their declarations (try flow):

1
2
3
let product: number | string = ...
let Component: mixed = ... // No good type to represent this! Consider restructuring
let element: React.Node = ...

Variables declared without initializers

Often variables are declared without initializers. In such cases, Flow will try to choose the “first” assignment or assignments to the variable to define its type. “First” here means both top-to-bottom and nearer-scope to deeper-scope—we’ll try to choose an assignment that happens in the same function scope as the variable’s declaration, and only look inside nested functions if we don’t find any assignments locally (try flow).

1
2
3
4
5
6
7
8
let topLevelAssigned;

function helper() {
  topLevelAssigned = 42; // Error: `topLevelAssigned` has type `string`
}

topLevelAssigned = "Hello world"; // This write determines the var's type
topLevelAssigned = true; // Error: `topLevelAssigned` has type `string`

If there are two or more possible “first assignments,” due to an if or switch statement, they’ll both count—this is one of the few ways that Flow will still infer unions for variable types (try flow):

1
2
3
4
5
6
7
8
9
10
11
let myNumberOrString;

if (condition) {
  myNumberOrString = 42; // Determines type
} else {
  myNumberOrString = "Hello world"; // Determines type
}

myNumberOrString = 21; // fine, compatible with type
myNumberOrString = "Goodbye"; // fine, compatible with type
myNumberOrString = false; // Error: `myNumberOrString` has type `number | string`

This only applies when the variable is written to in both branches, however. If only one branch contains a write, that write becomes the type of the variable afterwards (though Flow will still check to make sure that the variable is definitely initialized) (try flow):

1
2
3
4
5
6
7
8
let oneBranchAssigned;

if (condition) {
  oneBranchAssigned = "Hello world!";
}

oneBranchAssigned.toUpperCase(); // Error: `oneBranchAssigned` may be uninitialized
oneBranchAssigned = 42; // Error: `oneBranchAssigned` has type `string`

Variables initialized to null

Finally, the one exception to the general principle that variable’s types are determined by their first assignment(s) is when a variable is initialized as (or whose first assignment is) the literal value null. In such cases, the next non-null assignment (using the same rules as above) determines the rest of the variable’s type, and the overall type of the variable becomes a union of null and the type of the subsequent assignment. This supports the common pattern where a variable starts off as null before getting assigned by a value of some other type (try flow).

1
2
3
4
5
6
7
8
9
10
11
12
function findIDValue<T>(dict: {[key: string]: T}): T {
  let idVal = null; // initialized as `null`
  for (const key in dict) {
    if (key === 'ID') {
      idVal = dict[key]; // Infer that `idVal` has type `null | T`
    }
  }
  if (idVal === null) {
    throw new Error("No entry for ID!");
  }
  return idVal;
}

Was this guide helpful? Let us know by sending a message to @flowtype.