Back to Flow

Flow for TypeScript Users

website/docs/flow-vs-typescript.md

0.317.092.7 KB
Original Source

Flow and TypeScript share most of the same syntax, much of the same vocabulary, and a large set of overlapping concepts (conditional types, mapped types, type guards, keyof, as const, unknown, Readonly). The convergence is largely intentional: Flow's syntax has shifted to align with TypeScript's over the past several years. If you know TypeScript, your intuition will get you most of the way through a Flow program.

Where the two diverge, the divergence is usually a deliberate Flow choice in favor of stronger static guarantees. Flow rejects a number of patterns that TypeScript accepts but that can throw at runtime, silently corrupt values, or cause logic bugs. Flow also ships several features with no built-in TypeScript counterpart, covered in Flow-only concepts below. The most prominent is React: Flow has its own first-class component, hook, and renders syntax.

This page is organized in four sections:

Additional reference sections cover upcoming TS-aligned work, legacy Flow syntax convergence, shared config options, and external declaration mechanisms.

Scope note: Unlike tsc, the flow binary itself is a typechecker only: it doesn't emit JavaScript. Compiling Flow syntax to runtime JS is handled by separate build tools. TypeScript bundles both into one tool. TypeScript claims below verified against version 6.0.3 with strict enabled.

Concepts that transfer cleanly from TypeScript {#toc-transfer-cleanly}

The features below are close enough in syntax and semantics that you can reuse your TypeScript intuition more or less directly.

Two more syntax forms match TypeScript directly even though the underlying semantics diverge - see Variance.

  • Array shorthand T[] (in addition to Array<T>).
  • Variance keywords: readonly on properties and in / out on type parameters.

Combining several of these features, the function below type-checks identically in both languages:

js
type User = {
  readonly name: string,
  readonly age: number,
};

function get<K extends keyof User>(user: User, key: K): User[K] {
  return user[key];
}

declare const user: User;
const age: number = get(user, 'age');

Shared concepts, different rules {#toc-shared-concepts-different-rules}

This section is where Flow most often surprises a reader coming from TypeScript. Both languages have these concepts (objects, classes, variance, refinement, generics, module exports, suppressions), but Flow's rules diverge in ways that might not be apparent by reading the code.

In most subsections the syntax is the same but the semantics differ: code that's accepted by TypeScript but rejected by Flow as wrong, unsound, or unsafe - often throwing at runtime, silently corrupting values, or causing logic bugs. The rest are renamed spellings, validations Flow adds at module boundaries, or a different suppression form. The subsection headers tell you which case applies.

:::info Nominal vs. structural typing. TypeScript is primarily structural - two types with the same public shape are interchangeable, with narrow nominal carve-outs (#private fields, private/protected modifiers, and unique symbol). Flow is structural for plain objects and functions, but deliberately nominal for classes, opaque types, and Flow Enums.

Flow chose nominal typing here because identity carries real information in those constructs: for example, a UserId and a PostId can have the same underlying representation but are not interchangeable. Treating identity nominally rather than structurally lets the type system catch entire categories of logic bugs (right shape, wrong meaning) and lets users model their domain at the level of what something is, not just what it looks like. :::

Objects, classes, and interfaces {#toc-shape-rules}

How Flow types objects, classes, and interfaces: the exact-by-default object rule, nominal class identity, the asymmetric subtyping rules, primitive-to-interface assignability, type-level spread, the this-binding rules for class methods and object literals, and tuple spread.

SurfaceTypeScriptFlowDetails
Object types{x: number} allows extra properties except for fresh object-literal excess-property checks.{x: number} is exact by default; use {x: number, ...} when extra properties are allowed.Object exactness
Class / interface / object subtypingClasses, interfaces, and object types are mostly interchangeable by structure.Classes are nominal; object types accept only themselves; interfaces can accept all threeClasses are nominal
implements / extends argCan target object-shaped utility types like Pick<T, K> or Omit<T, K>.Must name an interface or class, not an object type alias.implements and extends clauses
Primitives vs interfacesPrimitives satisfy object/interface shapes that exist on the boxed prototype.Primitives are not subtypes of object types or interfaces.Primitives are not subtypes of interfaces or object types
Object combinationIntersections are the standard way to combine object types.Use object type spread ({...A, ...B}); intersections, while supported for inexact objects, don't work for exact objects.Object spread typing
this bindingsMethod extraction unsafely loses this; this in object literals is allowed.Class method extraction is rejected; this in object literals is banned.this is bound on class methods and banned in object literals
Tuple spread after an optional elementAllowed; the resulting tuple type doesn't match the runtime layout when the optional element is absent.Rejected when the source tuple's arity isn't statically fixed.Tuple spread after an optional element is banned

Object exactness {#toc-object-exactness}

In Flow, object types are exact by default: {x: number} admits exactly the property x and no others. To allow additional properties, write the inexact form with a trailing ...: {x: number, ...}. In TypeScript, object types are open at the type-system level - {x: number} allows additional properties, and the rule that catches extras is the excess-property check, which fires only on direct object-literal assignment.

That distinction matters because it explains why TypeScript code can look like it agrees with Flow's exactness when it doesn't: const o: {x: number} = {x: 1, y: 2} errors in TS too, but only because the literal is inlined. Indirect cases (assigning the literal to a variable first, passing it through a function, or other paths where the literal's "fresh" status is lost) type-check in TS and would not be exactness violations there. Flow's exactness applies uniformly regardless of binding shape, and shows up in two TS-shaped patterns below.

• Pattern: extra properties slip through indirect assignment.

ts
// TypeScript:
type Settings = {volume: number, brightness: number};
const raw = {volume: 0.5, brightness: 0.8, theme: "dark"};
const settings: Settings = raw;
Object.values(settings).map(v => v.toFixed(2)); // Runtime crash! `Settings` types every value as `number`, so `.toFixed` runs on the `"dark"` string

TypeScript accepts this: raw is inferred as {volume: number, brightness: number, theme: string}, which is structurally a subtype of {volume: number, brightness: number} because TS object types are open. The excess-property check does not fire on indirect assignment. Flow rejects the same code:

js
// Flow:
type Settings = {volume: number, brightness: number};
const raw = {volume: 0.5, brightness: 0.8, theme: "dark"};
const settings: Settings = raw; // ERROR

When the extra properties are intentional, the fix is the inexact form - write {volume: number, brightness: number, ...} so the type explicitly admits unknown additional properties:

js
// Flow:
type Settings = {volume: number, brightness: number, ...};
const raw = {volume: 0.5, brightness: 0.8, theme: "dark"};
const settings: Settings = raw; // OK

• Pattern: a property is forgotten and re-introduced at a different type. Typical when modeling a Flow function on a TypeScript signature that takes a "looser" type and adds optional fields. TypeScript allows the property to be forgotten via inexact subtyping and then re-introduced at a different (optional) type - leaving timestamp typed as number | undefined while it actually holds the string '2026-01-15':

ts
// TypeScript:
type LogEntry = {id: string, timestamp: string};
type WithUnixTime = {id: string, timestamp?: number};

const e: LogEntry = {id: 'a', timestamp: '2026-01-15'};
const base: {id: string} = e;       // `timestamp` "forgotten"
const widened: WithUnixTime = base; // `timestamp` re-introduced at a new type
widened.timestamp?.toFixed(); // Runtime crash! ?. only short-circuits on nullish; the value
                              // is the string '2026-01-15', so .toFixed runs and throws.

Flow blocks this path in two places: the literal translation fails at the forget step ({id: string} is exact by default), and modeling the forget with an inexact target pushes the failure to the re-introduce step. Exactness gates both directions: a property can only be forgotten when the target is inexact, and re-introduced only when the source is exact.

js
// Flow:
type LogEntry = {id: string, timestamp: string};
type WithUnixTime = {id: string, timestamp?: number};

declare const e: LogEntry;

// Literal Flow translation: fails at the forget step because `{id: string}` is exact.
const baseExact: {id: string} = e; // ERROR

// Modeled with inexact target: forget step passes, but re-introduction fails.
const baseInexact: {id: string, ...} = e;     // OK
const widened: WithUnixTime = baseInexact;    // ERROR

Classes are nominal; the object/interface/class subtyping is asymmetric {#toc-classes-nominal}

TypeScript types classes structurally: interface, type, and class instances are largely interchangeable as long as the shapes match. In Flow, an object type describes a plain object (specifically the shape produced by an object literal {...}), while an interface describes a contract that any plain object, class instance, or interface-typed value can satisfy. From those definitions, combined with Flow's nominal typing for classes (two distinct classes with the same members are not interchangeable), the one-way subtyping rules follow directly:

  • An object literal flows into an object type or an interface - its shape and kind are both fully known at the point of construction.
  • A class instance flows into an interface (the contract makes no claim about what backs it) but not into an object type (object types describe object literals, not instances).
  • An interface-typed value flows into another interface but not into an object type - it could be backed by a class instance, and the object type wouldn't accept that backing value.

Inexactness on the target ({a: number, ...}) widens which plain-object properties are allowed but doesn't widen the kinds of values accepted - object types only describe plain objects. Class instances and interfaces are different kinds, and are themselves always inexact (interfaces by definition; class instances because a subclass could add members), so they still fail against an inexact object type, just with a different diagnostic:

  • Against an exact object type (the default), the error is [incompatible-exact].
  • Against an inexact object type ({a: number, ...}), the error is [class-object-subtyping] with text "Class instances are not subtypes of object types; consider rewriting object type as an interface."
js
// Flow:
class Foo {
  a: number = 1;
}
interface I { a: number }
type Obj = {a: number, ...};

declare function acceptsInterface(x: I): void;
declare function acceptsObj(x: Obj): void;
declare const someI: I;

acceptsInterface(new Foo()); // OK
acceptsInterface({a: 1});    // OK
acceptsObj({a: 1});          // OK
acceptsObj(new Foo());       // ERROR
acceptsObj(someI);           // ERROR: same [class-object-subtyping] code

The standard fix when you hit this in Flow is to switch the parameter type from object type to interface.

A further consequence of the kind distinction: object types don't allow this (Flow rejects this in object literals with [object-this-reference]), so they describe plain data, not this-aware behavior. This is why implements/extends needs a class or interface and why method extraction is safe on object types but not on class instances.

One more note: TypeScript's structural treatment of classes is almost total: const c: C = {x: 1} type-checks in TS even though c is annotated as a class instance. TS has a handful of nominal carve-outs on top of the structural default: ECMAScript #private fields, the private / protected access modifiers (both of which block assignability across distinct declarations), and unique symbol - but they're exceptions to an otherwise structural model. Flow's class nominalism is total: no nominal opt-in is required because the class identity itself is the nominal channel. This is why Flow's class/object error fires far more often than a TS user would expect.

implements and extends clauses must name an interface or class {#toc-implements-extends-rhs}

The right-hand side of an implements or extends clause has stricter shape rules in Flow than in TypeScript. TypeScript lets you write class C implements Omit<HTMLAttrs, "k"> or interface I extends Pick<Y, K> - any object type works. Flow rejects object types in those clauses with their own diagnostics:

  • class C implements ObjType errors with [cannot-implement] "Cannot implement ObjType because it is not an interface."
  • interface I extends ObjType errors with [incompatible-use] "Cannot extend ObjType ... because ObjType is not inheritable."

The standard Flow rewrite is to introduce an interface (interface I { a: number; b: string } then class C implements I) or to inline the members directly. Mapped/utility types applied to interfaces work in Flow but the result is an object type, which is exactly what these clauses won't accept - so the rewrite needs to land at an interface, not at an object-typed alias.

js
// Flow:
type ObjType = {a: number, b: string};
class C implements ObjType { // ERROR: [cannot-implement]
  a: number = 1;
  b: string = "hi";
}
interface I extends ObjType { // ERROR: [incompatible-use]
  c: boolean;
}

Primitives are not subtypes of interfaces or object types {#toc-primitives-interfaces}

TypeScript treats string / number / boolean as structurally assignable to any interface or object type they satisfy: the primitive is checked against the members of its corresponding boxed prototype (String.prototype / Number.prototype / Boolean.prototype), so a string satisfies any interface whose members exist on String.prototype (e.g. {length: number}, Iterable<string>). No runtime boxing is implied: the compatibility is purely at the type level. Flow does not perform that check in .js files: a primitive flowing into an interface errors with [incompatible-type] ("Cannot use string as a subtype of interface"), and a primitive flowing into an object type errors with the generic [incompatible-type] code.

The runtime consequence is usually a logic bug. Iterable<string> is satisfied by both Array<string> and string: TS accepts the latter, so a caller can silently pass a bare string where a list of strings was intended. A for...of loop then yields characters instead of items - rarely what anyone actually meant.

ts
// TypeScript:
function logAll(items: Iterable<string>) {
  for (const x of items) console.log(x);
}
logAll(["foo", "bar"]); // logs "foo" then "bar"
logAll("fb");           // String.prototype[Symbol.iterator] yields characters,
                        // so the loop walks code points: logs "f" then "b" instead of items

TypeScript accepts this via wrapper-promotion: String.prototype[Symbol.iterator] satisfies Iterable<string>, so a string flows in just as an array does. Flow rejects:

js
// Flow:
function logAll(items: Iterable<string>) {
  for (const x of items) console.log(x);
}
const msgs = "foo";
logAll(msgs); // ERROR: `string` is not a subtype of `Iterable<string>`

The Flow rewrite is at the call site: if a one-element list was intended, wrap the string in [s]; otherwise the call was the wrong shape.

Object spread typing {#toc-type-spread}

Flow lifts {...A, b: T} to the type level: type C = {...A, b: T} is a real type annotation that combines A's properties with b: T. TypeScript has no type-level spread; it uses intersection (type C = A & {b: T}) instead.

This isn't a stylistic choice: it falls out of exact object types. Because Flow's exact object types forbid unlisted properties, intersecting two exact object types produces an impossible type: a value would have to be exactly A and exactly B simultaneously, which is impossible to satisfy as soon as A and B differ at all (see impossible intersection types). So Flow needs a different operation to combine exact object types. Type-level spread ({...A, ...B}) is that operation, and it mirrors the runtime semantics of value-level spread directly: own properties only (so interfaces can't be spread, since they don't track own-vs-prototype), later keys overwrite earlier ones, and spreading an inexact type requires the result to also be marked inexact (with the spread before any named properties); otherwise Flow rejects the spread, since unknown properties from the source can't be soundly admitted into an exact target.

js
// Flow:
type A = {x: number, y: string};
type C = {...A, z: boolean};
const c: C = {x: 1, y: "hi", z: true};

Flow's typing of value-level spread is also safer. TypeScript types {...c} as if it copies everything in the interface, but if the value is backed by a class, prototype methods aren't actually on the spread result at runtime, so the typing is unsound:

ts
// TypeScript:
interface Counter { count: number; inc(): number }
class Impl {
  count = 0;
  inc(): number { return ++this.count; }
}
const c: Counter = new Impl();
const o = {...c};
o.inc(); // Runtime crash! `inc` is on `Impl.prototype`, not on the spread result

Flow rejects with [cannot-spread-interface] because interfaces don't track own-vs-prototype, so the result type can't be sound:

js
// Flow:
interface Counter { count: number; inc(): number }
declare const c: Counter;
const o = {...c}; // ERROR: [cannot-spread-interface]

this is bound on class methods and banned in object literals {#toc-method-unbinding}

Flow's two this-related rules both head off the same runtime crash: a method body running with this undefined. The class case rejects the extraction so the call stays bound to its receiver; the object-literal case rejects the construct so there's no this-aware method on a plain object to extract in the first place.

this usageMethod extraction
ClassesAllowedBanned
Object literalsBannedAllowed

• Method extraction from a class instance. TypeScript treats methods as plain function values and lets them be extracted silently: calling the extracted version runs the method body with this undefined, and any access through this.field crashes.

ts
// TypeScript:
class Counter {
  count = 0;
  increment(): number { return ++this.count; }
}
const counter = new Counter();
const tick = counter.increment;
tick(); // Runtime crash! `this` is undefined, so `++this.count` throws

Flow rejects the extraction with [method-unbinding] ("Cannot get counter.increment because property increment cannot be unbound from the context where it was defined.") because it tracks the this binding on method-shorthand properties of a class:

js
// Flow:
class Counter {
  count: number = 0;
  increment(): number { return ++this.count; }
}
const counter = new Counter();
const tick = counter.increment; // ERROR: [method-unbinding]
const tickFixed = () => counter.increment(); // OK - arrow captures `this`
tickFixed(); // OK

The Flow rewrite is to wrap with an arrow function that captures this, as shown by tickFixed above.

this inside an object literal. TypeScript allows this references inside object-literal methods (TS infers this as the enclosing literal's type); the same extraction hazard then applies at the call site.

ts
// TypeScript:
const counter = {
  count: 0,
  increment(): number { return ++this.count; }
};
const tick = counter.increment;
tick(); // Runtime crash! `this` is undefined, so `++this.count` throws

Flow forbids the construct itself with [object-this-reference], so the unsafe extraction never gets the chance to be attempted:

js
// Flow:
const counter = {
  count: 0,
  increment(): number { return ++this.count; } // ERROR: [object-this-reference]
};

The Flow rewrite is to use the name of the object literal binding directly instead of this.

js
// Flow:
const counter = {
  count: 0,
  increment(): number { return ++counter.count; } // Use object name directly instead of `this`
};
const tick = counter.increment; // Extraction allowed: no `this` in the function
tick(); // OK

Method-shorthand on plain object types ({m(x: number): number}) carries no this context to lose, so extraction is fine there; this is also why a class instance can't flow into an object type but an object literal can.

Tuple spread after an optional element is banned {#toc-tuple-spread-optional}

Spreading a tuple type with optional elements into another tuple is allowed in TypeScript but produces an inaccurate tuple type: if the optional element is absent at runtime, the trailing positions shift left and the slots TS computed don't match the runtime layout.

ts
// TypeScript:
const middle: [middleName?: string] = [];
const fullName: [string, string | undefined, string] = ["Ada", ...middle, "Lovelace"];
fullName[2].toUpperCase(); // Runtime crash! `fullName[2]` is undefined, not "Lovelace"

The TS type is statically known (fullName is annotated as [string, string | undefined, string] and that annotation is accepted), but it is unsound: when middle is empty at runtime, the value at position 1 is "Lovelace" (shifted) and position 2 is absent, so the tuple TS computed does not match the runtime layout. Flow rejects the spread with [invalid-tuple-arity] ("array literal has an unknown number of elements") because the source tuple's arity is not statically fixed, so no sound static tuple shape can be produced for the result:

js
// Flow:
const middle: [middleName?: string] = [];
const fullName: [string, string | void, string] = ["Ada", ...middle, "Lovelace"]; // ERROR

The Flow rewrite is to branch explicitly on whether the optional element is present and assemble each shape on its own arm.

Type spellings {#toc-type-spellings}

How Flow spells absent or nullable types and the top and bottom of the type hierarchy. These are name-only divergences: same concepts, different spellings.

ConceptTypeScriptFlowNote
Type inhabited only by undefinedundefinedvoidFlow has no separate undefined type. (details)
"No useful value" return markervoidvoidSame name; Flow has only void (see above).
Nullable value (T | null | undefined)T | null | undefined?T (shorthand for T | null | void)T | void alone lacks null.
Bottom typeneveremptynever is the natural TS reach when Flow expects empty.
Top typeunknownunknownSame name.

See type hierarchy for where these sit relative to the rest of Flow's types.

void vs undefined {#toc-void-vs-undefined}

undefined as an annotation is a hard error in Flow:

js
// Flow:
function f(): undefined { // ERROR: [unsupported-syntax]
  return undefined;
}
js
// Flow:
function f(): void {
  return undefined; // OK - `undefined` is the value inhabiting `void`
}

This comes up most often when a TS-shaped function signature gets typed in Flow verbatim, and on TS utility-typed code (Exclude<T, undefined>, T extends undefined ? ...) where the undefined literal type appears inside a generic. Standard Flow forms: undefinedvoid for annotations; T | undefined?T if null is also intended (most JS APIs) or T | void if only the absent case is intended.

A related TS quirk worth flagging: TypeScript's () => undefined and () => void are assignably asymmetric (undefined returns satisfy void slots but not vice versa). Flow has no equivalent since there's only void.

A parameter type that includes void (whether spelled T | void, ?T, or T | null | void) makes the argument implicitly optional, so callers can omit it entirely. This differs from TypeScript, where (x: T | undefined) still requires the call site to pass undefined.

js
// Flow:
function f(x: ?number) {}
f(null);      // OK
f(undefined); // OK
f();          // OK - `?T` includes `void`, which makes the arg optional

Variance {#toc-variance}

Flow's variance defaults are stricter than TypeScript's. The subsections below cover the keyword syntax for opting in or out and the positions where the defaults diverge.

SurfaceTypeScriptFlowDetails
Variance keywordsUses readonly properties and in / out type parameters; can also spell explicit invariance as <in out T>.Uses readonly / writeonly properties and in / out type parameters; default type-parameter variance is invariant.Variance keywords
Mutable object propertiesCovariant.Invariant.Mutable object properties
readonly property assignabilityreadonly and mutable properties are assignable in ways that can drop the read-only constraint.readonly cannot be dropped by assigning to a mutable-property type.readonly properties
Mutable arraysCovariant.Invariant.Mutable arrays
Generic type argumentsVariance is inferred from usage with compatibility-oriented exceptions.Invariant by default unless declared out or in.Generic type arguments
Method parametersBivariant for method syntax; function-typed fields are contravariant.Contravariant.Method parameters
this type positionsAllowed in input and invariant (mutable field) positions.Restricted to covariant positions (return types and readonly fields).this type positions

:::info Variance - a quick overview. Variance describes how subtyping flows through a position where a type T appears - for example, the property type in {x: T}, a function parameter or return type, or a generic argument like Container<T>. Given that Sub is a subtype of Super, that position is:

  • Covariant: preserves direction. A {readonly x: Sub} is a subtype of {readonly x: Super}. The right choice for read-only positions and function return types.
  • Contravariant: reverses direction. A function (x: Super) => void is a subtype of (x: Sub) => void - a callee that accepts wider inputs satisfies a caller passing narrower ones.
  • Invariant: neither direction; the position can't soundly widen or narrow. The required default whenever a slot is both read and written (e.g., a mutable {x: T}), since covariance breaks writes and contravariance breaks reads.
  • Bivariant: both directions accepted. Usually unsound; TypeScript permits it in a few places (notably method parameters). Flow never uses bivariance.

Flow defaults each position to the strictest sound choice; TypeScript defaults to looser ones at several positions, which is the entire reason this section exists. :::

Variance keywords (readonly / writeonly, in / out) {#toc-variance-keywords}

Flow's standard syntax for variance uses the TS-aligned keyword forms: readonly / writeonly on properties and in / out on type parameters. Note that writeonly is Flow-specific: TypeScript has no write-only equivalent.

In the other direction, TypeScript's combined <in out T> (explicit invariance) has no Flow counterpart. TypeScript infers variance from usage and preserves several compatibility-oriented exceptions, so users sometimes need to opt back into invariance to recover the stricter guarantee they wanted; Flow's default is invariance, so the stricter choice is what you get when you write nothing. See Generic type arguments below for the defaults contrast in detail.

Beyond the spelling, Flow validates that a type parameter declared out T (or in T) is only used in body positions that match the declared variance - out T in an input position errors [incompatible-variance] "Cannot use T in an input position because T is expected to occur only in output positions." TypeScript also validates in / out against the body in many positions (e.g. interface Box<out T> { set: (t: T) => void } errors in TS too, since the function-typed field puts T contravariantly - function inputs flip variance). The narrower gap is that TS keeps method shorthand bivariant even under an out/in annotation, so the Flow form below (written with method shorthand) errors in Flow but compiles in TS.

js
// Flow:
type Box<out T> = {
  set(t: T): void; // ERROR: [incompatible-variance]
};

This subsection is about the syntax; for the much more important semantic divergence in how variance is enforced at each position, see the next subsection. See the variance docs for full mechanics.

Each subsection below is a place where Flow picks the stricter sound default and TypeScript picks the looser one. Together they are the largest single cluster of TypeScript code that type-checks but relies on weaker static guarantees - every example accepts a program that can throw at runtime or silently corrupt values.

Mutable object properties are invariant in Flow, covariant in TS {#toc-variance-mutable-props}

Assigning {price: number} to {price: number | string} widens the slot's read type but also widens what can be written into it, so a downstream p.price = "Free!" lands a string in the caller's number-typed property.

ts
// TypeScript:
function markFree(p: {price: number | string}) {
  p.price = "Free!";
}
const item: {price: number} = {price: 9.99};
markFree(item);
item.price.toFixed(2); // Runtime crash! `.toFixed` is not a function on `"Free!"`

TypeScript allows the call: the property is covariant, so {price: number} is treated as a subtype of {price: number | string}. The mutation inside markFree then writes a string into the caller's number-typed slot. Flow rejects:

js
// Flow:
function markFree(p: {price: number | string}) {
  p.price = "Free!";
}
const item: {price: number} = {price: 9.99};
markFree(item); // ERROR: property `price` is invariantly typed

The fix depends on whether the function genuinely needs to mutate. If it doesn't, make the parameter read-only (either with the Readonly<T> utility or the readonly property modifier), and removing the possibility of mutation through p makes the widening safe. If it does (as markFree here), the caller has to provide a source whose annotation already includes the wider type. TypeScript supports the same readonly property modifier, but see the next sub-bullet for how the two languages diverge on enforcing it.

js
// Flow:
function logPrice(p: Readonly<{price: number | string}>) {}
const item: {price: number} = {price: 9.99};
logPrice(item); // OK

readonly properties are interchangeable with mutable in TS, but not in Flow {#toc-variance-readonly-props}

Assigning a {readonly value: T} to {value: T} would let a caller drop the read-only constraint and mutate the underlying object.

ts
// TypeScript:
function f(obj: {value: number}) {
  obj.value = 99; // Silent mutation of a slot the caller annotated `readonly`
}
const o: {readonly value: number} = {value: 1};
f(o);

TypeScript allows the call; the mutation through f succeeds at runtime. TypeScript enforces readonly at direct write sites, but assignability can drop the readonly constraint. Flow treats readonly / writeonly as load-bearing for static safety and rejects:

js
// Flow:
function f(obj: {value: number}) {
  obj.value = 99;
}
const o: {readonly value: number} = {value: 1};
f(o); // ERROR: [incompatible-variance]

The fix is to also mark the target read-only ({readonly value: number}) - once f declares it won't mutate, dropping the constraint is no longer at issue and the call succeeds. If f genuinely needs to mutate, the caller has to provide a mutable source instead.

js
// Flow:
function f(obj: {readonly value: number}) {}
const o: {readonly value: number} = {value: 1};
f(o); // OK

Mutable arrays are invariant in Flow, covariant in TS {#toc-variance-arrays}

Treating Array<string> as Array<string | Error> lets a .push(new Error(...)) inside the callee plant an Error into the caller's string-typed array.

ts
// TypeScript:
function appendError(es: Array<string | Error>) {
  es.push(new Error("oops"));
}
const errors: Array<string> = [];
appendError(errors);
errors[0].toUpperCase(); // Runtime crash! `errors[0].toUpperCase` is not a function

TypeScript allows the call. The push goes through, and the typed-as-string element at index 0 is actually an Error instance at runtime. Flow rejects:

js
// Flow:
function appendError(es: Array<string | Error>) {
  es.push(new Error("oops"));
}
const errors: Array<string> = [];
appendError(errors); // ERROR: `Array<string>` is invariantly typed

The fix depends on whether the callee needs to mutate. If it doesn't, switch the parameter to ReadonlyArray<T> - removing the possibility of mutation through es makes the widening safe. ReadonlyArray exists in Flow precisely because the mutable form is invariant - a fact often missed when reaching for the covariant TS pattern. If the callee genuinely needs to mutate (as appendError does), the caller has to supply an array whose element type already includes the wider option.

js
// Flow:
function logErrors(es: ReadonlyArray<string | Error>) {}
const errors: Array<string> = [];
logErrors(errors); // OK

Generic type arguments are invariant by default {#toc-variance-generics}

Flow defaults generic parameters to invariance and asks the user to opt into co/contravariance with out T / in T. TypeScript infers variance from usage and preserves compatibility-oriented exceptions, which can leave read-write fields with weaker static guarantees than the Flow default.

ts
// TypeScript:
class Box<T> {
  value: T;
  constructor(value: T) { this.value = value; }
}
function widen(b: Box<number | string>) {
  b.value = "oh no";
}
const box: Box<number> = new Box(1);
widen(box);
box.value.toFixed(); // Runtime crash! `box.value` is now "oh no", not a number

TypeScript allows the call; widen then writes a string into the caller's number-typed slot. Flow rejects:

js
// Flow:
class Box<T> {
  value: T;
  constructor(value: T) { this.value = value; }
}
function widen(b: Box<number | string>) {}
const box: Box<number> = new Box(1);
widen(box); // ERROR

When writing a Flow generic in this shape: either the field is genuinely read-only (mark it readonly and the parameter out) or it is not, in which case Flow's invariance is correct.

Method parameters are contravariant in Flow but bivariant in TS {#toc-variance-methods}

TypeScript's method-parameter variance is bivariant: {compare(a: string, b: string): number} is treated as a subtype of {compare(a: string | number, b: string | number): number}, so the wider type can pass numbers into a body that only handles strings, crashing at runtime. (TypeScript's function-typed fields are contravariant: this method-vs-field asymmetry is itself a TS-only wrinkle.)

ts
// TypeScript:
type StringComparator = {compare(a: string, b: string): number};
type Comparator = {compare(a: string | number, b: string | number): number};

function callCompare(c: Comparator) {
  c.compare(1, 2); // Runtime crash! body calls .localeCompare on a number
}
const stringComparator: StringComparator = {
  compare(a, b) { return a.localeCompare(b); }
};
callCompare(stringComparator);

The bivariance hole is invisible at the call site: TS users see no error until runtime. Flow rejects the call statically, catching the bug before it can crash:

js
// Flow:
type StringComparator = {compare(a: string, b: string): number};
type Comparator = {compare(a: string | number, b: string | number): number};

function callCompare(c: Comparator) {}
const stringComparator: StringComparator = {
  compare(a, b) { return a.localeCompare(b); }
};
callCompare(stringComparator); // ERROR

Flow's specific error depends on whether compare is method shorthand or a function-typed field. Method shorthand (as above) fails contravariance ([incompatible-type] "the first parameter: string is incompatible with number"): function inputs flip variance, so widening them is unsound.

Switching from method shorthand to a mutable function field makes the check stricter rather than looser. The property itself is now mutable, so the error becomes invariance (the property is invariantly typed), which blocks the opposite (safe) direction too.

Adding readonly compare restores that safe direction (a Comparator-typed value flowing into a StringComparator slot) by making the property covariant, but it does not fix the example above: function-input contravariance is still what blocks widening the inputs, and the only way to accept wider inputs is to declare compare with those wider inputs to begin with.

See the variance docs and the subtyping docs for the full mechanics.

this type is restricted to covariant positions {#toc-this-variance}

The this type is used for fluent APIs and polymorphic method receivers. In covariant positions (return types, and readonly fields in Flow), both languages accept it: a method declared m(): this preserves the subclass type through fluent chains, so new SubBuilder().add(1).extra() keeps its SubBuilder type. The divergence is in input and invariant (mutable field) positions. TypeScript accepts this there too, but a writable slot typed this lets a caller write a parent-class instance into a subclass-typed field; later access that calls a subclass-only method crashes:

ts
// TypeScript:
class Builder {
  parent: this | null = null;
}
class SubBuilder extends Builder {
  extra(): this { return this; }
}
function stash(b: Builder) {
  b.parent = new Builder(); // `b.parent` is `Builder | null` here, so the write is allowed
}
const sb = new SubBuilder();
stash(sb); // `SubBuilder` flows into `Builder`; the write inside `stash` lands on `sb.parent`
if (sb.parent !== null) {
  // The null check only excludes null; it doesn't verify the class identity.
  // The unsound write in `stash` put a `Builder` in a slot typed `this` (= `SubBuilder`).
  sb.parent.extra(); // Runtime crash! `sb.parent` is a `Builder`, not a `SubBuilder`
}

Flow rejects this in both input and invariant (mutable field) positions with [incompatible-variance]. This falls out of the same variance model that makes mutable object properties and mutable arrays invariant:

js
// Flow:
class Builder {
  add(x: number): this { return this; } // OK: return type is covariant
  readonly origin: this | null = null; // OK: readonly field is covariant
  takesSelf(other: this): void {} // ERROR: input position
  parent: this | null = null; // ERROR: invariant field
}

The rewrite when you hit this is to name the class explicitly in the input/field position (other: Builder, parent: Builder | null) and accept the loss of the subclass type at that slot, or to make the field readonly so the position becomes covariant.

Refinement and module-level validation {#toc-validation}

How Flow validates the body of type guards, when refinements are invalidated by intervening code, and the validation Flow performs at module boundaries (annotation requirements and the value/type seam).

User-defined type guard bodies are validated {#toc-type-guard-validation}

TypeScript checks the signature of an x is T predicate - it requires the predicate type to be assignable to the parameter type, so function f(x: string): x is number is rejected at declaration. But TypeScript does not check that the function body actually implements the claimed refinement. The body is trusted, so the following type-checks in TypeScript even though typeof x === "object" && x !== null is true for any non-null object ({} included), and any caller relying on this guard will be lied to:

ts
// TypeScript:
type User = {id: string, name: string};
function isUser(x: unknown): x is User {
  return typeof x === "object" && x !== null;
}
const data: unknown = {};
if (isUser(data)) {
  data.name.toUpperCase(); // Runtime crash! `data` has no `name` property
}

Flow validates the body of a type-guard function in both directions. Each direction surfaces with its own diagnostic.

• Positive direction ([incompatible-type-guard]). At every return expression, the type of the refined parameter must be a subtype of the guard type. So the equivalent of the TypeScript example above is rejected:

js
// Flow:
type User = {id: string, name: string};
function isUser(x: unknown): x is User {
  return typeof x === "object" && x !== null; // ERROR: refined type isn't a subtype of `User`
}

• Negative direction ([incompatible-type-guard]). When the predicate returns false, the negation must completely refine away the guard type from the parameter - otherwise a caller could see a value that should have been excluded. A predicate typed as x is A that actually checks x instanceof B (a strict subtype) is rejected for this reason:

js
// Flow:
class A {}
class B extends A {}
function isA(x: unknown): x is A {
  return x instanceof B; // ERROR: negation does not refine `A` away
}

The diagnostic explicitly suggests the escape hatch: "Consider using a one-sided type-guard (implies x is T)." One-sided guards (implies x is T) skip exactly this negation check - they refine the parameter to T when the function returns true and leave it unchanged when it returns false, which is the right shape when only the positive direction holds.

See the type guards docs for the full consistency rules, and the one-sided type guard section below for the implies form.

Refinement invalidation rules differ {#toc-refinement-invalidation}

Both Flow and TypeScript narrow types via typeof, instanceof, equality, type guards, etc. - but the rules for when a refinement is dropped diverge in ways that aren't apparent from reading the code. Flow invalidates a refinement when intervening code could have changed the underlying value at that storage location:

  • A write to the refined binding or property (x = ..., obj.k = ...).
  • A refinement on an object property where the property is reachable through aliasing or could be mutated by a callee.
  • A refinement on a binding captured by a closure that an intervening call could invoke.

A bare call to a function that does not visibly touch the refined location does not by itself drop a refinement on a local: that is the most common over-correction. TypeScript's narrowing has its own (also non-trivial) invalidation model that does not agree with Flow's in detail; the same code may type-check in TS and not in Flow, or vice versa. See refinement invalidations for the full rule set.

js
// Flow:
declare function sideEffect(): void;

function localCase(x: ?number) {
  if (x != null) {
    sideEffect();        // bare call does NOT drop the refinement on a local
    const a: number = x; // OK
  }
}

function propertyCase(obj: {x: ?number}) {
  if (obj.x != null) {
    sideEffect();            // bare call DROPS the refinement on a property
    const a: number = obj.x; // ERROR: callee could have mutated `obj.x`
  }
}

function writeCase(x: ?number) {
  if (x != null) {
    x = null;
    const a: number = x; // ERROR: `x` is now typed `null` after the write
  }
}

The standard fix for the property case is to extract the refined value to a local before any intervening code - once it's a local, the bare-call exemption above applies and the refinement survives. The write case is fixed by not reassigning the refined binding; use a separate local for the new value instead.

js
// Flow:
declare function sideEffect(): void;

function propertyCaseFixed(obj: {x: ?number}) {
  const {x} = obj;
  if (x != null) {
    sideEffect();
    const a: number = x; // OK - local refinement survives the call
  }
}

Annotations are required at module boundaries {#toc-annotations-boundaries}

Flow requires annotations on function parameters, exports, and other key boundaries, and reports [signature-verification-failure] if a module's exports cannot be typed from annotations alone. If you're used to leaving exports unannotated and letting the typechecker infer them across modules, that will not work in Flow: the annotations have to be there. TypeScript offers an opt-in equivalent (--isolatedDeclarations, TS 5.5+), but it's not part of strict and is off by default.

This is a deliberate design choice that enables Flow to scale to repositories with millions of files. Because each module's exports are fully described by its annotations, Flow can extract a "typed interface" for the module without analyzing the module body, then typecheck every other module against that interface in parallel.

See the annotation requirement docs and the Module Exports subsection for full mechanics.

js
// Flow:
// ERROR: return type inferred, not annotated.
export function getUser(id: string) { // [signature-verification-failure]
  return {id, name: 'Alice', age: 30};
}

// OK - annotate the return so the module's typed interface is self-contained.
export function getUser(id: string): {id: string, name: string, age: number} {
  return {id, name: 'Alice', age: 30};
}

Type-only bindings cannot cross the value/type seam {#toc-type-export-validation}

TypeScript's import type and export type are, by default, erasable annotations: the underlying classification happens at use site, so a value-position import {Foo} of a type-only export silently resolves as a type. (Under --verbatimModuleSyntax, TS does require import type at the import site and errors otherwise - but that's an opt-in mode, not the default.) Flow validates the value/type kind at the import and export site unconditionally, with two distinct diagnostics:

  • A value-position import {Foo} from a module that only export typed Foo errors [import-type-as-value] "Cannot import the type Foo as a value. Use import type instead."
  • A value-position export {Foo} where Foo is a type-only binding in the current module errors [type-as-value] "Cannot use type Foo as a value. Types are erased and don't exist at runtime."

The fix in both cases is the explicit type form: import type {Foo} or export type {Foo}. This is load-bearing for Flow's signature-extraction model: the typed interface a module exposes has to be unambiguous about which exports are types and which are values, since dependents are checked against that interface in parallel without analyzing the module body.

js
// Flow:
// mod.js
export type Foo = {x: number};
js
// Flow:
// consumer.js
import {Foo} from './mod'; // ERROR: [import-type-as-value]
import type {Foo as FooType} from './mod'; // OK

Explicit type controls {#toc-explicit-controls}

Three places where TypeScript accepts a looser surface spelling and Flow requires the explicit form: as casts, error suppressions, and generic-argument lists.

as casts are safer in Flow {#toc-as-casts}

Flow's as only widens or asserts (e.g. 42 as number, 42 as 42), and rejects unsafe downcasts at the type level - {id: 1} as {id: number, name: string} is a Flow error, not a cast. TypeScript's as accepts any cast where the two types are assignable in either direction, which lets it silently approve unsafe downcasts. The same TypeScript line type-checks even though the cast invents a name: string property that doesn't exist at runtime, and accessing it then crashes at runtime. This permissiveness is the single biggest source of "TS code that looks like it should work in Flow but doesn't."

ts
// TypeScript:
const u = {id: 1} as {id: number, name: string};
u.name.toUpperCase(); // Runtime crash! `u.name` is undefined

Flow accepts the widening and assertion uses, but rejects unsafe downcasts:

js
// Flow:
const n = 42 as number; // OK - widens the literal `42` to `number`
const exact = 42 as 42; // OK - asserts at the literal type
const u = {id: 1} as {id: number, name: string}; // ERROR - unsafe downcast

Flow's escape hatch for a forced cast is the explicit value as any as T two-step; TypeScript's idiom is value as unknown as T.

Error suppressions are coded and scoped {#toc-suppressions}

Flow's $FlowFixMe[code] (and $FlowExpectedError[code]) suppresses only the named error code at that location: any other error on the same line still surfaces, and the suppression itself is reported as an unused-suppression warning if the targeted code doesn't fire. TypeScript's // @ts-ignore silences every error on the next line indiscriminately, and // @ts-expect-error similarly silences everything but errors when nothing was suppressed. The Flow form is strictly more granular and keeps suppression debt easy to track. See the errors docs.

js
// Flow:
const o: {x: number} = {x: 1};
// $FlowFixMe[prop-missing] - intentional for demo
const y = o.nonexistent;

Generic type arguments cannot be omitted {#toc-generic-default-omission}

TypeScript lets you write a generic type unparameterized when every type parameter has a default: Foo<T = string> followed by type A = Foo resolves A to Foo<string>. Flow rejects the bare form and requires an explicit type-argument list (or an empty <> to fall back on defaults), reporting [missing-type-arg] "Cannot use Foo without 0-1 type arguments."

js
// Flow:
type Foo<T = string> = {x: T};
type A = Foo;   // ERROR: [missing-type-arg]
type B = Foo<>; // OK    - uses the default `T = string`

The rewrite is mechanical: FooFoo<> for all-defaulted generics, or supply the args explicitly. The rationale for the explicit form is that Flow reserves the bare name Foo for the type constructor itself (so that operations on the type, such as type-level functions, can take the unapplied form as input), rather than overloading it as shorthand for an applied instantiation.

Flow-only concepts with no built-in TypeScript equivalent {#toc-flow-only}

These are the constructs Flow has built that TypeScript hasn't built into the language or typechecker: most of them because the underlying problem (React component shapes, hook rules, render constraints, exhaustive pattern matching, nominal abstraction across module boundaries, runtime-and-type-level enums) is one TypeScript leaves to framework/library patterns, lint rules, or user code. There is no built-in TypeScript equivalent to translate from, only a Flow concept to learn fresh.

component syntax {#toc-component-syntax}

Flow ships first-class component syntax for declaring React components. There is no built-in TypeScript equivalent; TypeScript models components as plain functions. What the component keyword buys over a plain function:

  • Individual named params instead of a props object. Removes the destructuring-and-typing duplication of ({name}: {name: string}) and the need to wrap props in Readonly<{...}> - component params are read-only by default.
  • No return type annotation. Flow infers and enforces React.Node, and rejects components that implicitly return.
  • Optional renders clause that constrains what JSX shape the component is allowed to produce - covered in its own section below.
  • Structural rules enforced at parse/type-check: no this, no nested component definitions, and components can only be rendered as JSX, not called as plain functions.
js
// Flow:
import * as React from 'react';

component Greeting(name: string, greeting: string = "Hello") {
  return <p>{greeting}, {name}!</p>;
}

component App() {
  return <Greeting name="World" />;
}

TypeScript models the same shape with a plain function component plus a props type. Unlike Flow's component, the resulting value is also callable as a plain function:

tsx
// TypeScript:
import * as React from 'react';

type GreetingProps = {
  name: string,
  greeting?: string,
};
function Greeting({name, greeting = "Hello"}: GreetingProps) {
  return <p>{greeting}, {name}!</p>;
}

function App() {
  return <Greeting name="World" />;
}

const elem = Greeting({name: "World"}); // No error: TS treats components as callable

Nested component definitions and function-style calls surface as [nested-component] and [react-rule-call-component] respectively:

js
// Flow:
import * as React from 'react';

component Outer() {
  component Inner() { // ERROR: components may not be nested within other components or hooks
    return <div />;
  }
  return <Inner />;
}

const x = Outer({}); // ERROR: components cannot be called; use JSX (<Outer />) instead

hook syntax {#toc-hook-syntax}

hook syntax is a first-class Flow keyword for declaring React hooks. Flow uses the keyword to enforce the Rules of React at the type level on hook call sites. TypeScript has no equivalent: hook rules in TS are enforced by ESLint (eslint-plugin-react-hooks), which operates on AST patterns without type information or whole-program analysis.

Flow tracks hook-ness as part of the function type, which lets it catch violations that an AST-based linter can't reach. Below, useToggle is passed where a non-hook function is expected; Flow errors with [react-rule-hook-incompatible]. The call site has no syntactic clue that fn will be invoked as a hook inside the callee, so ESLint's AST pattern matching can't flag it:

js
// Flow:
import {useState} from 'react';

hook useToggle(initial: boolean): [boolean, () => void] {
  const [v, sv] = useState(initial);
  return [v, () => sv(x => !x)];
}

function callIt(fn: (boolean) => [boolean, () => void]): [boolean, () => void] {
  return fn(false);
}

callIt(useToggle); // ERROR: `useToggle` is a hook; `fn` is not

TypeScript has no concept of hook-ness; useToggle is just a function. The call below type-checks even though callIt invokes useToggle outside a component or hook at runtime, and eslint-plugin-react-hooks doesn't catch it either - the call site has no syntactic clue that fn will be invoked as a hook inside the callee:

ts
// TypeScript:
import {useState} from 'react';

function useToggle(initial: boolean): [boolean, () => void] {
  const [v, sv] = useState(initial);
  return [v, () => sv(x => !x)];
}

function callIt(fn: (b: boolean) => [boolean, () => void]): [boolean, () => void] {
  return fn(false);
}

callIt(useToggle); // No type error; rule-of-hooks violation surfaces at runtime

renders types {#toc-renders-types}

Render types (renders, renders?, renders*) declare composition contracts for components: what slots accept and what components produce. Design systems and libraries can constrain composition across wrapper components and HOCs, with the type checker rejecting violations. There is no built-in TypeScript equivalent.

js
// Flow:
import * as React from 'react';

component Header(text: string, color: string) {
  return <div style={{color}}>{text}</div>;
}
component MainHeader(text: string) renders Header {
  return <Header text={text} color="red" />;
}

component Layout(header: renders Header) {
  return <div>
    {header}
    <section>Content</section>
  </div>;
}

const ok = <Layout header={<MainHeader text="Flow" />} />;
const bad = <Layout header={<footer />} />; // ERROR: `<footer />` does not satisfy `renders Header`

TypeScript has no equivalent. The closest is typing slots as React.ReactNode, which accepts any node - no composition contract is expressible, so the <footer /> case below is accepted just like the intended <MainHeader />:

tsx
// TypeScript:
import * as React from 'react';

function Header({text, color}: {text: string, color: string}) {
  return <div style={{color}}>{text}</div>;
}
function MainHeader({text}: {text: string}) {
  return <Header text={text} color="red" />;
}

function Layout({header}: {header: React.ReactNode}) {
  return <div>{header}<section>Content</section></div>;
}

const ok = <Layout header={<MainHeader text="TS" />} />;
const bad = <Layout header={<footer />} />; // No type error

match expressions and statements {#toc-match}

Flow has match expressions and statements for pattern matching with structural patterns, guards, and exhaustiveness checking. TypeScript has no match; the closest equivalent for the statement form is a hand-coded discriminated-union switch with an assertNever fallthrough. The expression form has no direct TS equivalent at all, because switch is statement-only in JavaScript - TS users typically reach for nested ternaries or an IIFE wrapping a switch, both of which lose the structural patterns, guards, and exhaustiveness checks match provides.

js
// Flow:
type Action =
  | {type: 'add', text: string}
  | {type: 'toggle', id: string}
  | {type: 'remove', id: string}
  | {type: 'filter', mode: 'all' | 'active' | 'done'};

declare const action: Action;

const description: string = match (action) {
  {type: 'add', const text} => `Add: ${text}`,
  {type: 'toggle', const id} => `Toggle ${id}`,
  {type: 'remove', const id} => `Remove ${id}`,
  {type: 'filter', mode: 'all'} => 'Show all',
  {type: 'filter', mode: 'active'} => 'Show active',
  {type: 'filter', mode: 'done'} => 'Show done',
};

TypeScript has no match. Nested ternaries are the closest expression-form equivalent - structural patterns, in-pattern variable extraction, and exhaustiveness checks are all gone:

ts
// TypeScript:
type Action =
  | {type: 'add', text: string}
  | {type: 'toggle', id: string}
  | {type: 'remove', id: string}
  | {type: 'filter', mode: 'all' | 'active' | 'done'};

declare const action: Action;

const description: string =
  action.type === 'add' ? `Add: ${action.text}` :
  action.type === 'toggle' ? `Toggle ${action.id}` :
  action.type === 'remove' ? `Remove ${action.id}` :
  action.type === 'filter' && action.mode === 'all' ? 'Show all' :
  action.type === 'filter' && action.mode === 'active' ? 'Show active' :
  action.type === 'filter' && action.mode === 'done' ? 'Show done' :
  (() => { throw new Error('unreachable'); })();

match is exhaustively checked: omitting a case is [match-not-exhaustive], and the error names the missing pattern. Adding a new action type or a new filter mode then surfaces every site that hasn't handled it:

js
// Flow:
type Action =
  | {type: 'add', text: string}
  | {type: 'toggle', id: string}
  | {type: 'remove', id: string}
  | {type: 'filter', mode: 'all' | 'active' | 'done'};

declare const action: Action;

const description: string = match (action) { // ERROR: missing `{type: 'filter', mode: 'done'}`
  {type: 'add', const text} => `Add: ${text}`,
  {type: 'toggle', const id} => `Toggle ${id}`,
  {type: 'remove', const id} => `Remove ${id}`,
  {type: 'filter', mode: 'all'} => 'Show all',
  {type: 'filter', mode: 'active'} => 'Show active',
};

Opaque types {#toc-opaque-types}

Opaque type aliases hide their underlying type outside the file in which they are defined, enforcing nominal abstraction across module boundaries. TypeScript has no native equivalent; the common idiom there is "branded types" using intersection with a (typically unique symbol-keyed) marker property, which is a userland pattern rather than a language feature.

The boundary the branded types idiom enforces is weaker than Flow's file-scoped abstraction: a single as cast is enough to produce a branded value. 42 as UserId type-checks in TypeScript because the source (number) and the target (number & {readonly [brand]: 'UserId'}) overlap on number, and TS only rejects an as cast when the two sides are disjoint.

Flow's opaque types, by contrast, are sealed by the module boundary itself: outside the defining file, the underlying type is not visible at all, so as widening cannot produce the opaque type from its underlying representation.

js
// Flow:
opaque type UserId = number;

declare function makeUserId(n: number): UserId;
declare function lookupUser(id: UserId): string;

const id: UserId = makeUserId(42);
lookupUser(id); // OK
// In another file, `42` is not a `UserId` and a `UserId` is not a `number`.
// Inside this file (where the underlying type is visible) the conversion is allowed:
const n: number = id;

The view from another file: outside the defining module, the underlying type is sealed. declare opaque type UserId (the shape the importer sees) makes the seal visible - both the structural construction 42 and the projection back to number are rejected:

js
// Flow (importer's view, as if `UserId` and functions were imported from another file):
declare opaque type UserId;
declare function makeUserId(n: number): UserId;
declare function lookupUser(id: UserId): string;

const id: UserId = makeUserId(42);
lookupUser(id); // OK
const forged: UserId = 42;    // ERROR: number is not a UserId
const n: number = id;         // ERROR: UserId is not a number

The TypeScript branded types encoding, with the unsafe as cast at the bottom:

ts
// TypeScript:
declare const brand: unique symbol;
type UserId = number & {readonly [brand]: 'UserId'};

declare function makeUserId(n: number): UserId;
declare function lookupUser(id: UserId): string;

const id: UserId = makeUserId(42);
lookupUser(id); // OK

// `as` cast: source and target overlap on `number`, so TS accepts.
const forgedByCast = 42 as UserId;
lookupUser(forgedByCast);

Flow Enums {#toc-flow-enums}

Flow Enums and TypeScript enum look superficially similar but are very different in detail.

AspectTypeScriptFlow
Exhaustive switchNo built-in diagnostic; encoded with never or lint.Built-in: [invalid-exhaustive-check] if a member is forgotten.
Implicit coercion to/from underlying primitivePermits number → number-enum slots (except non-matching literals) and freely coerces enums to numbers.Blocked both directions; use .cast() to convert in, and value as <representation type> (for example as string / as number) to convert out.
Default member valuesNumber enums auto-number from 0.Number-enum members must be explicitly initialized; string enums default to mirroring member names.
Re-declarationAllowed; can collide with default values silently.[name-already-bound].
Reverse mappingNumber enums get a runtime reverse-map; string enums error on the same access..getName(value) works for both number and string enums.
Iterating membersObject.values(Status) is the natural value-iteration form, but on a number enum it returns both the values and the member-name strings from the runtime reverse-map.Status.members() returns just the values.
Symbol enumsNone.Supported (enum X of symbol { ... }).
Definition restrictionsPermits heterogeneous initializers, non-literal initializers, and lowercase-leading member names.All three error.

A few of these have rationales worth knowing:

  • The default-value rule exists because adding or removing a member from the middle of an auto-numbered enum silently renumbers everything after it, which is a serialization/logging hazard.
  • The TS string-enum reverse-mapping error is structural: StatusStr.Off has literal type "off" (the value), not "Off" (the key), so StatusStr[StatusStr.Off] resolves to a non-existent StatusStr["off"].
  • TS Object.values over a 3-member number enum produces [ 'Active', 'Paused', 'Off', 0, 1, 2 ] - both halves of the runtime reverse-map land in the result (and for...in over the same enum exposes the same duplication on its keys).
  • Lowercase-leading member names are reserved because Flow Enums expose lowercase methods like .cast and .members.

Exhaustiveness is built in: omitting a case in a switch over a Flow Enum is [invalid-exhaustive-check], with the missing member named. Adding a new member then surfaces every site that hasn't handled it:

js
// Flow:
enum Status {
  Active,
  Paused,
  Off,
}

declare const st: Status;

let label: string;
switch (st) { // ERROR: member `Off` has not been considered
  case Status.Active: label = 'on'; break;
  case Status.Paused: label = 'wait'; break;
}

See the Flow Enums docs for full mechanics.

One-sided type guards (implies) {#toc-one-sided-guards}

A predicate function whose return type is implies param is T refines the parameter to T only when the function returns true, and leaves it unchanged when the function returns false. This is the escape hatch for the body-validation rule covered above when only the positive direction holds. TypeScript has no equivalent.

For example, isPositive(n: ?number) returns true only for positive numbers. A two-sided n is number would be unsound here: the false branch can be null, void, or a non-positive number like 0 or -1, but ?number minus number only leaves null | void - silently losing the non-positive case. The implies form says only the positive direction refines, so the else branch keeps the original ?number type.

js
// Flow:
function isPositive(n: ?number): implies n is number {
  return n != null && n > 0;
}

declare const n: ?number;
if (isPositive(n)) {
  n as number; // OK: refined to number
} else {
  n as ?number; // OK: stays ?number (not narrowed to null | void)
}

TypeScript has only two-sided type guards. The else branch always refines the predicate type away, even when that refinement isn't sound. The code below type-checks, but the inferred else-branch type is wrong - n could actually be a non-positive number at runtime:

ts
// TypeScript:
function isPositive(n: number | null | undefined): n is number {
  return n != null && n > 0;
}

declare const n: number | null | undefined;
if (isPositive(n)) {
  // n: number
} else {
  // TS narrows n to `null | undefined` here, even though a non-positive
  // number (e.g. 0, -1) would also reach this branch at runtime.
}

import typeof {#toc-import-typeof}

import typeof is the Flow-only form. import type Foo from './m' (which Flow shares with TypeScript: both languages support it) brings in the type of a type export; import typeof Foo from './m' is Flow-specific and brings in the type of a value export so it can be used as a type annotation.

TypeScript's nearest equivalent for import typeof is typeof import('./m'), but the binding shape differs: TypeScript produces the namespace shape and is usually combined with indexed access (typeof import('./m')['Foo']), while import typeof Foo from './m' binds a single value's type as a top-level type binding.

The example below uses both forms: import type {Node} pulls in a type export directly as a type annotation, while import typeof {useState} pulls in the type of a value export. A generic value's type stays parameterizable through import typeof - you can write useState<number> against the imported type, instantiating the imported function type to its number specialization, so the hook parameter is callable as a number-typed useState.

js
// Flow:
import type {Node as ReactNode} from 'react';
const node: ReactNode = "hello, world";

import typeof {useState} from 'react';
hook useCounter(useStateNum: useState<number>): number {
  const [count, setCount] = useStateNum(0);
  setCount(c => c + 1);
  return count;
}

Flow-only utility types {#toc-flow-only-utilities}

A handful of utility types have no TypeScript counterpart. The closest TS spellings (where one exists) are noted below; the rest have no native TS form and are typically encoded with userland patterns.

  • Class<T> - the type of the class constructor for an instance type T. No TS native form; the usual TS encoding is new (...args: any[]) => T or typeof T for a specific class.
  • Values<T> - the union of value types of T's properties. TS spelling is the indexed access T[keyof T].
  • $KeyMirror<O> - an object type whose property values are string-literal types mirroring their keys. No TS native form.
  • $Exports<'mod'> - the type of a module's exports given a path string. TS's nearest equivalent is typeof import('mod'), with a different shape.
  • StringPrefix<P> / StringSuffix<S> - strings constrained to a literal prefix or suffix. The TS equivalent is template literal types (`${P}${string}` / `${string}${S}`), which Flow does not yet have (see Coming soon).
  • $Exact<T> - promotes an inexact object type to exact. Discouraged in new code; object types are exact by default, so this is only useful when wrapping an inexact alias.

Flow-only syntactic forms {#toc-flow-only-syntax}

A handful of Flow type-annotation forms have no TypeScript spelling - tsc rejects them at parse time. They are alternate syntax for existing Flow concepts.

  • Inline interface type annotation - type T = interface { foo: number }. Lets an interface appear inside a type expression instead of as a top-level declaration. TypeScript requires a separate interface I { ... } statement.
  • Optional indexed access type - Obj?.['prop'] mirrors the runtime ?. operator at the type level: if Obj is nullish, the result is void; otherwise it is Obj['prop']. TypeScript has no type-level ?..
  • Anonymous function-type parameters - type F = string => void. Flow lets you omit the parameter name when it carries no information; TypeScript requires (x: string) => void.
  • Anonymous indexer parameters - type O = {[string]: number}. Same shape: Flow omits the index-key name when it isn't referenced; TypeScript requires {[k: string]: number}.
js
// Flow:
type Inline = interface { foo: number };
type Opt = ?{foo: number};
type Pulled = Opt?.['foo']; // number | void
type Fn = string => void;
type Dict = {[string]: number};

Relay / GraphQL integration {#toc-relay-integration}

Setting relay_integration=true in [options] makes Flow natively understand graphql tagged template literals and infer their types from the Relay compiler's emitted artifacts, so users can omit explicit type parameters on useFragment, usePreloadedQuery, etc. Companion options: relay_integration.esmodules (resolve artifacts as ES module default exports rather than CommonJS) and relay_integration.excludes (per-directory opt-out). See the docs for this option.

TypeScript has no typechecker-level equivalent. TypeScript users either pass the generated type explicitly (useFragment<MyFragment$key>(...)), use a TypeScript language service plugin for editor hints (not typechecking), or use document-node patterns like graphql-typed-document-node / gql.tada that require explicit imports of generated types.

With relay_integration=true, Flow reads the Relay compiler's emitted artifact for MyFragment and infers the key and result types from the graphql tag - no type parameter, no generated-type import:

js
// Flow:
import {graphql, useFragment} from 'react-relay';
declare const userRef: MyFragment$key;

const data = useFragment(
  graphql`fragment MyFragment on User { name }`,
  userRef,
);

TypeScript requires the explicit type parameter and a generated-type import:

ts
// TypeScript:
import {graphql, useFragment} from 'react-relay';
import type {MyFragment$key} from './__generated__/MyFragment.graphql';
declare const userRef: MyFragment$key;

const data = useFragment<MyFragment$key>(
  graphql`fragment MyFragment on User { name }`,
  userRef,
);

TypeScript-only features that do not exist in Flow {#toc-ts-only}

These are TypeScript features that have no Flow equivalent today. Some Flow has deliberately not adopted, either because they overlap a Flow feature with different (usually more conservative) defaults or because they introduce footguns Flow's design avoids. Others are simply not implemented yet (see the separate Coming soon section for features in progress or pending a release gate). Reaching for any of the items below in Flow code won't work, and in some cases the TypeScript syntax will parse, so the failure shows up later than expected.

TS-only syntactic forms {#toc-ts-only-syntax}

A handful of TS surface-syntax forms have no Flow spelling, but the concept is available in Flow under a different name. Flow rejects the TS form at parse/type-check time with a diagnostic that points at the Flow rewrite directly.

  • Angle-bracket type assertion - TS <T>x → Flow x as T.
  • Optional unlabeled tuple elements - TS [number, string?] → Flow [a: number, b?: string]. Flow requires the labeled variant for optional elements.
  • readonly type operator on tuples - TS readonly [T, S] → Flow Readonly<[T, S]>.
  • readonly type operator on array shorthand - TS readonly T[] → Flow ReadonlyArray<T>.

Note that readonly as a property modifier ({readonly x: T}) and on type parameters (out T) works the same way in both languages - see Variance keywords. The two readonly forms above are uses of readonly as a type operator (a prefix on a structural type), which is a TS-only spelling: Flow uses the wrapper utility instead.

Decorators {#toc-decorators}

Flow parses decorator syntax but does not type-check it: the decorator's type is never applied to the underlying value, and the decoration is silently erased. TypeScript supports two incompatible modes: TC39 decorators (the default, with a context-object parameter) and legacy decorators (under --experimentalDecorators, with the old (target, key) signature).

TypeScript class syntax extensions {#toc-class-extensions}

TypeScript has several class-syntax extensions Flow has deliberately not adopted, asking users to write the equivalent JS instead.

  • Parameter properties (constructor(public x: number)) - a TS-only shorthand that emits runtime code: it auto-declares the field and assigns it from the constructor argument. Flow's diagnostic: "Flow does not support TypeScript parameter properties. To fix, declare the property in the class body and assign it in the constructor."

  • public / protected / private access modifiers - TS-checked access control. These are type-checker-only in TypeScript (the field is still publicly accessible at runtime), so dropping them is safe. Flow rejects all three; drop them (public / protected carry no runtime effect) or migrate private foo to #foo. The #private rewrite lands at a different runtime shape from the TS form: ECMAScript #private fields are nominally private at runtime, while TS private is erased.

  • accessor auto-accessors (class C { accessor x: T = init }) - a TC39 proposal that desugars to a paired getter/setter backed by a private field. Flow does not parse the form. Write the getter and setter explicitly with a #private backing field, or use a plain field if no accessor wrapping is needed.

Runtime namespace blocks {#toc-namespace-blocks}

No source-level namespace { ... } blocks. Flow has declare namespace for ambient declarations inside libdefs, but not source-level namespace blocks that produce runtime values.

const enum {#toc-const-enum}

No equivalent. Flow's transforms emit Flow Enums as a runtime object rather than inlining them at the use site like tsc does for const enum. Their restrictions (literal-only values, no redeclaration, no default number values) still make build-system inlining straightforward if you want it.

Assertion functions {#toc-assertion-functions}

TypeScript's asserts x is T return type declares a function that throws when the assertion fails and refines the parameter to T unconditionally after the call returns: a different shape from a type guard, which returns a boolean and refines only inside an if/else. Flow has type guards (x is T) but no asserts x is T form.

The closest Flow equivalent is a type guard combined with an explicit throw at the call site: function isStr(x: unknown): x is string { ... } then if (!isStr(x)) throw new Error();.

ThisType<T> utility {#toc-this-type-utility}

TypeScript's ThisType<T> is a marker used inside a contextual type to rewire this to T within the methods of an object literal. Flow does not implement that rewiring. The two language differences that make ThisType<T> useful in TypeScript are absent in Flow:

  • Object literals reject this references outright, so there is no object-literal method body whose this Flow could rewire.
  • Class and interface methods have a fixed this binding tied to their declaring type, which cannot be reassigned by an external marker.

Expressions with type arguments {#toc-expression-type-args}

TypeScript accepts type arguments on a value expression: Foo<string> as a standalone expression specializes the generic and can be bound to a name. Flow does not parse the form and errors with a ParseError shortly after the closing >.

ts
// TypeScript:
declare class Foo<T> {
  value: T;
}
const StringFoo = Foo<string>;

The Flow rewrite is to supply the type arguments at the instantiation or call site (new Foo<string>(), f<string>(x)) rather than naming a pre-specialized binding.

Sentinel refinement through destructured values {#toc-sentinel-destructure}

TypeScript narrows destructured properties of a discriminated union together: refining the sentinel binding also refines the other bindings extracted in the same destructuring.

ts
// TypeScript:
type FormField =
  | {kind: 'text', value: string}
  | {kind: 'number', value: number};

declare const field: FormField;
const {kind, value} = field;
if (kind === 'text') {
  const s: string = value; // OK - TS narrows `value` based on `kind`
} else {
  const n: number = value;
}

Flow refines sentinel-tagged unions through the original value (if (field.kind === 'text') { ... field.value ... }), but each destructured binding carries its full union type independent of the others, so the same code fails:

js
// Flow:
type FormField =
  | {kind: 'text', value: string}
  | {kind: 'number', value: number};

declare const field: FormField;
const {kind, value} = field;
if (kind === 'text') {
  const s: string = value; // ERROR: `value` keeps its full `string | number` type
}

The Flow rewrite is to refine through the original value rather than destructure. Alternatively, a match expression or statement handles matching and destructuring in one form - each arm matches the sentinel and binds value at its per-arm type:

js
// Flow:
type FormField =
  | {kind: 'text', value: string}
  | {kind: 'number', value: number};

declare const field: FormField;
match (field) {
  {kind: 'text', const value} => {
    const s: string = value;
  }
  {kind: 'number', const value} => {
    const n: number = value;
  }
}

User-side module augmentation {#toc-module-augmentation}

No equivalent at the source level. TypeScript users routinely re-open third-party modules from source code via declare module 'name' { ... } to add types. Flow's declare module is only used inside libdefs under flow-typed/, not from arbitrary source files.

Coming soon {#toc-coming-soon}

The following TypeScript features have type-checking support implemented in Flow, but are waiting for tooling updates (e.g. Prettier) before Flow removes the release gate. They are enabled in the Flow playground.

  • Template literal types - e.g. `${'a' | 'b'}-${'x' | 'y'}`.
  • Mapped type modifiers - variance removal -readonly and as key remapping.
  • Constructor types - type Ctor = new (x: number) => R.
  • satisfies expression - validates an expression against a type without widening the inferred type.
  • Inline import() type expression - type A = import('./m').A.
  • import X = require('foo') and export = X - CommonJS-style import and export bindings.

Planned:

  • Abstract classes and methods.
  • override on class members.
  • Support for symbol-keyed property accesses at the type level. The unique symbol syntax parses today, but the type system doesn't yet model symbol keys as distinct nominal keys.

Syntax convergence with TypeScript {#toc-convergence}

This table maps legacy Flow forms to their modern Flow replacements. Some rows are syntax renames; others are older utilities or features with TS-aligned equivalents. New code should use the right-hand form.

Legacy FlowModern Flow (TS-aligned)
mixedunknown
$Keys<T>keyof T
$ReadOnly<T>Readonly<T>
$NonMaybeType<T>NonNullable<T>
$ReadOnlyArray<T>ReadonlyArray<T>
<T: Bound><T extends Bound>
(x: T) castx as T
{| a: number |} exact{a: number} (exact is the default)
+foo / -foo property variancereadonly foo / writeonly foo (writeonly is Flow-specific)
+T / -T type parameter varianceout T / in T
%checks predicate functionsuser-defined type guards (function isString(x: unknown): x is string)
$ObjMap<O, F> / $ObjMapi<O, F> / $TupleMap<T, F> / $TupleMapi<T, F>mapped types ({[K in keyof O]: ...}) with the function body inlined
$PropertyType<T, K> / $ElementType<T, K>indexed access (T[K])
$Call<F, ...Args>ReturnType<F> plus indexed access, or a conditional type with infer
$Diff<A, B> / $Rest<A, B>typically Omit<A, keyof B>, case by case (not always semantically identical)

For the full picture see Modernizing Legacy Flow Syntax.

Config options aligned with TypeScript {#toc-shared-options}

A few Flow .flowconfig [options] toggles correspond directly to TypeScript compilerOptions strictness flags - same semantics, but different defaults. In the TypeScript strict baseline used on this page, useUnknownInCatchVariables is enabled through strict, while noUncheckedIndexedAccess is not part of strict and stays opt-in. Flow has no strict options umbrella - both flags are opt-in individually and default to false, so porting from a TS project with strict enabled means turning use_unknown_in_catch_variables on to match.

TypeScript optionFlow optionDescription
noUncheckedIndexedAccessno_unchecked_indexed_accessIndexed access through an array or dictionary widens the result type with undefined (Flow: void), so reading arr[i] or dict[k] returns T | void instead of T and forces the caller to refine before use. Tuple access with a literal index is unaffected in both languages. See docs for more.
useUnknownInCatchVariablesuse_unknown_in_catch_variablesChanges the default type of an un-annotated catch binding from any to unknown. The caller has to narrow the value (instanceof Error, typeof e === 'string', …) before using it. See docs for more.

Typing external code {#toc-declaration-mechanisms}

TypeScript's .d.ts files cover two distinct concerns: typing third-party npm packages and typing first-party (or vendored) code that must remain plain JavaScript. Flow splits these into two mechanisms.

TypeScript use caseFlow mechanismPlacement and resolution
Third-party package declarations, including @types/* packages and package-level .d.ts files.Library definitions (libdefs).Plain .js files in flow-typed/ that usually name packages with declare module 'pkg' { ... }.
A sibling declaration file next to a JavaScript implementation.Declaration files.Colocated .js.flow or .json.flow files (for example, Misc.js.flow next to Misc.js) that shadow the implementation file.
Source-level module augmentation with declare module 'pkg' { ... } from arbitrary project files.No source-level equivalent.Flow's declare module 'pkg' { ... } form is for libdefs under flow-typed/, not for reopening modules from ordinary source files or colocated .js.flow declaration files.

The two Flow mechanisms share much of TypeScript's ambient declaration syntax, but placement is load-bearing. declare class, declare function, declare const, and related forms can describe ambient values in libdefs, declaration files, or inline declarations; declare module 'name' { ... } is the named-package form used by libdefs. Declaration files usually describe the colocated module's exports directly, for example with declare export ... or declare module.exports.

See User-side module augmentation for the declaration-file pattern TypeScript supports that Flow does not.

Declaration merging is partially supported {#toc-declaration-merging}

Flow uses the same split-namespace model as TypeScript and supports a subset of declaration merging. Each name independently inhabits a value namespace and a type namespace, so a single identifier can be both a value and a type without colliding: const A = 1; interface A {} is accepted, value-side uses of A resolve to the const, type-side uses resolve to the interface. Constructs usable in both namespaces (classes, enums) register once in the value namespace and the type side falls back to it.

What Flow merges:

  • interface + interface - members union (compatible duplicates allowed; conflicting members error). In library definition files, extends lists also concatenate, call signatures overload as intersections, and type-param arity mismatches error. In regular source files, the merge is limited to members; extends lists and call signatures don't combine across declarations, and same-named interfaces can't both be exported from a file.
  • declare class + interface - interface members fold into the class (either order).
  • function / declare function + declare namespace - the namespace's type members fold into the function (either order), accessible as fn.T.
  • class / declare class + declare namespace - the namespace's type members fold into the class (either order), accessible as Cls.T.

What Flow does not do:

  • Runtime-merging. Only the type members of a declare namespace reliably propagate to a sibling function or class; value members are not treated as runtime properties on the host. TS-style function + namespace value-side merging where the namespace contributes runtime members is not supported.
  • Multi-block declare module merging. Multiple declare module 'name' { ... } blocks for the same module do not union; the second is treated as an override of the first. A libdef for a given module should live in one place.
  • User-side declare module 'name' { ... } source-level augmentation (see User-side module augmentation).

Generating declaration files {#toc-generating-declarations}

The mechanisms above are about declarations as input: typing code the typechecker can't otherwise see. The reverse direction is emitting declaration files from source. TypeScript handles this in the compiler itself: tsc --declaration emits a .d.ts alongside each compiled .ts, and --emitDeclarationOnly produces declarations without the corresponding .js. Flow has no equivalent built into the flow binary; the separate flow-api-translator NPM package fills this gap, producing .js.flow or .d.ts files from a Flow source file.

See also {#toc-see-also}

  • Glossary - carries a one-line TypeScript note on concepts that have one, and serves as a quick index when you only need to look up a single term.
  • Modernizing Legacy Flow Syntax - the full reference for migrating Flow's legacy $-prefixed utilities and other older syntactic forms to their modern (often TS-aligned) equivalents.