Flow for TypeScript Users
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, keyof, as const, unknown, Readonly, generics, type guards). 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 or leave the program with inaccurate static types.
React is one notable area where Flow does not mirror TypeScript: Flow ships its own first-class component, hook, and renders syntax instead of modeling components through function types, forwardRef, and framework/library patterns — covered in Flow-only concepts below.
This page is organized in four buckets:
- Concepts that transfer cleanly from TypeScript — start here for syntax and concepts you can reuse directly.
- Shared concepts, different rules — if TypeScript-shaped code is rejected by Flow, these are the most likely culprits.
- Flow-only concepts with no built-in TypeScript analogue.
- TypeScript-only features that do not exist in Flow.
After those comparison buckets, the page closes with cross-cutting reference sections on upcoming TS-aligned work, legacy Flow syntax convergence, shared config options, and external declaration mechanisms.
Scope note: Flow is a typechecker only — the
flowbinary doesn't emit JavaScript. TypeScript is both a typechecker and a compiler. See Getting Started for setting up Flow. TypeScript claims below verified against version 6.0.3 withstrictenabled.
Concepts that transfer cleanly from TypeScript
The features below are close enough in syntax and semantics that you can reuse your TypeScript intuition more or less directly.
- Conditional types with
infer. - Mapped types.
- Type guards of the form
param is T(with additional validations), including inferred type predicates. T[K]indexed access types.keyof Toperator.as constassertions.consttype parameters —function f<const T>(x: T): T.unknowntop type.- Generic bounds with
<T extends Bound>. - Array shorthand
T[](in addition toArray<T>). - Utility types:
Readonly,ReadonlyArray,ReadonlyMap,ReadonlySet,Pick,Omit,Record,Partial,Required,Exclude,Extract,NonNullable,Parameters,ReturnType,Awaited,ThisParameterType,OmitThisParameter,NoInfer. - Type-only imports and exports —
import typeandexport type. - Function-overload encoding via intersection —
((x: number) => string) & ((x: string) => number)resolves the per-call return type by argument type in both languages. - Ambient declaration forms like
declare const,declare let,declare class, anddeclare function. - JSX type-argument syntax at call sites —
<Box<number> value={42} />.
Shared concepts, different rules
This is the bucket 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 have little or no surface signal.
Most subsections are same-syntax-different-semantics: code that compiles in TypeScript but is wrong, unsound, or rejected in Flow. The rest are renamed spellings, validations Flow adds at module boundaries, or a different suppression form. Each is a small, mechanical adjustment from the TS form, and the subsection headers below tell you which case applies.
- Objects, classes, and interfaces - exact objects, nominal classes, asymmetric class/object/interface subtyping
- Type spellings -
voidvsundefined,?T,empty,unknown - Variance -
readonly,writeonly,in, andout - Refinement and module-level validation - validated type-guard bodies, refinement invalidation, module-boundary annotations
- Explicit type controls -
ascasts, error suppressions, and type argument omission
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.
The reason is that identity carries real information at those boundaries — a UserId is not a PostId, a Celsius is not a Fahrenheit, two distinct classes with the same fields are different concepts. Treating identity nominally rather than structurally lets the type system catch entire categories of logic bugs (the "right shape, wrong meaning" class of mistakes) and lets users model their domain at the level of what something is, not just what it looks like. The Classes are nominal subsection below is the concrete instance of this stance; opaque types and Flow Enums (covered later in Flow-only concepts) are the others.
Objects, classes, and interfaces
How Flow types objects, classes, and interfaces — the exact-by-default object rule, nominal class identity, the asymmetric subtyping rules, primitive-to-interface assignability, optional re-introduction, type-level spread, tuple spread, and class-method this binding.
| Surface | TypeScript | Flow | Details |
|---|---|---|---|
| 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 subtyping | Classes, interfaces, and object types are mostly interchangeable by structure. | Classes are nominal; object types accept only themselves; interfaces can accept all three | Classes are nominal |
implements / extends arg | Can 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 interfaces | Primitives 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 combination | Intersections 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 type spread is type-level in Flow |
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 v: T = {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.
1type T = {x: number};2const extra = {x: 1, y: 2};3const v: T = extra; // ERRORincompatible-typeCannot assign extra to v because property y is extra in object literal [1] but missing in T [2]. Exact objects do not accept extra props.// TypeScript accepts this — `extra` is inferred as `{x: number, y: number}`,
// which is structurally a subtype of `{x: number}` because TS object types
// are open. The excess-property check does not fire on indirect assignment.
type T = {x: number};
const extra = {x: 1, y: 2};
const v: T = extra;
When the extra properties are intentional, the fix is the inexact form — write {x: number, ...} so the type explicitly admits unknown additional properties:
1type T = {x: number, ...};2const extra = {x: 1, y: 2};3const v: T = extra; // OKClasses are nominal; the object/interface/class subtyping is asymmetric
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 value can satisfy, plain object or class instance. 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 triangle follows 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 ({a: number, ...}) widens the set of additional plain-object properties allowed; it does not widen the set of kinds of values accepted, so the class-instance and interface cases 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."
1class Foo {2 a: number = 1;3}4interface I { a: number }5type Obj = {a: number, ...};6
7declare function acceptsInterface(x: I): void;8declare function acceptsObj(x: Obj): void;9declare const someI: I;10
11acceptsInterface(new Foo()); // OK12acceptsInterface({a: 1}); // OK13acceptsObj({a: 1}); // OK14acceptsObj(new Foo()); // ERRORclass-object-subtypingCannot call acceptsObj with new Foo() bound to x because Foo [1] is not a subtype of Obj [2]. Class instances are not subtypes of object types; consider rewriting Obj [2] as an interface.15acceptsObj(someI); // ERROR — same code as aboveclass-object-subtypingCannot call acceptsObj with someI bound to x because I [1] is not a subtype of Obj [2]. Class instances are not subtypes of object types; consider rewriting Obj [2] as an interface.The canonical fix when you hit this in Flow is to switch the parameter type from object type to interface.
One more direction-of-travel note: TypeScript's class-structurality is almost total — const c: C = {x: 1} type-checks in TS even though c is annotated as a class instance. TS preserves a handful of nominal channels 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 carve-outs from 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 the inverse experience would suggest.
implements and extends clauses must name an interface or class
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 ObjTypeerrors with[cannot-implement]"Cannot implementObjTypebecause it is not an interface."interface I extends ObjTypeerrors with[incompatible-use]"Cannot extendObjType... becauseObjTypeis not inheritable."
The canonical 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.
1type ObjType = {a: number, b: string};2class C implements ObjType { // ERROR — [cannot-implement]cannot-implementCannot implement ObjType because it is not an interface.3 a: number = 1;4 b: string = "hi";5}6interface I extends ObjType { // ERROR — [incompatible-use]incompatible-useCannot extend ObjType [1] with I because ObjType [1] is not inheritable.7 c: boolean;8}Primitives are not subtypes of interfaces or object types
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}, {charAt(i: number): 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.
1interface HasLength { length: number }2const s: string = "abc";3const i: HasLength = s; // ERRORincompatible-typeCannot assign s to i because string [1], a primitive, cannot be used as a subtype of HasLength [2]. You can wrap it in new String(...)) to turn it into an object and attempt to use it as a subtype of an interface.4const o: {length: number, ...} = s; // ERRORincompatible-typeCannot assign s to o because string [1] is incompatible with object type [2].// TypeScript accepts both via wrapper-promotion.
interface HasLength { length: number }
const s: string = "abc";
const i: HasLength = s;
const o: {length: number} = s;
The Flow rewrite is to construct the interface-shaped value explicitly (const i: HasLength = {length: s.length}) or to read the property directly off the primitive (s.length) rather than asserting structural compatibility.
A useful corollary: TypeScript's object type (any non-primitive value) maps to Flow's interface {} — the empty interface accepts any object, array, or class instance and rejects primitives for the same reason described above.
Optional properties cannot be silently re-introduced
TypeScript allows a property to be forgotten via inexact subtyping and then re-introduced at a different (optional) type — leaving y typed as number | undefined while it actually holds the string "Uh oh":
// TypeScript:
const a: {x: number, y: string} = {x: 1, y: "Uh oh"};
const b: {x: number} = a; // y "forgotten"
const c: {x: number, y?: number} = b; // y re-introduced at a new type
// c.y has static type `number | undefined` but holds "Uh oh" at runtime.
Flow blocks this path in two places. A literal Flow translation already fails at const b: {x: number} = a, because {x: number} is exact by default. If you intentionally model the TypeScript "forget y" step with an inexact target ({x: number, ...}), Flow still rejects the next assignment, where y would be re-introduced at a different optional type. Exactness gates both directions: a property can only be forgotten when the target is inexact, and re-introduced only when the source is exact. This is the same underlying mechanism as object exactness showing up in a different shape — typical when modeling a Flow function on a TypeScript signature that takes a "looser" type and adds optional fields.
Object type spread is type-level in Flow, value-level only in TS
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 uninhabitable 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 exactness propagates — spreading an inexact type forces the result inexact, since the source could carry unknown properties.
The intersection form A & {b: T} is the natural reach if you're thinking in TypeScript, but it's the wrong tool for the job in Flow: & keeps its TypeScript intersection semantics, so writing A & {b: T} when A already declares b silently produces an uninhabitable type rather than the merged shape you wanted. The Flow idiom is {...A, b: T} — same shape as runtime spread, accurate semantics, no accidental impossibility. The objects docs cover the full spread rules.
1type A = {x: number, y: string};2type C = {...A, z: boolean};3const c: C = {x: 1, y: "hi", z: true};Tuple spread after an optional element is banned
Spreading a tuple type with optional elements into another tuple is allowed in TypeScript but produces an inaccurate tuple type: const x: [a?: 1] = []; const y: [0, 1 | undefined, 2] = [0, ...x, 2]; compiles in TS but y[2] is undefined at runtime. Flow rejects the spread with [invalid-tuple-arity] ("array literal has an unknown number of elements").
1const x: [a?: 1] = [];2const y: [0, 1 | void, 2] = [0, ...x, 2]; // ERRORinvalid-tuple-arityCannot assign array literal to y because array literal [1] has an unknown number of elements, so is incompatible with tuple type [2].The TS type is statically known — y is annotated as [0, 1 | undefined, 2] and that annotation is accepted — but it is unsound: when x is empty at runtime, the value at position 1 is 2 (shifted) and position 2 is absent, so the tuple TS computed does not match the runtime layout. Flow rejects the spread because the source tuple's arity is not statically fixed, so no sound static tuple shape can be produced for the result. The Flow rewrite is to branch explicitly on whether the optional element is present and assemble each shape on its own arm.
Class methods cannot be unbound from their this
Method-shorthand properties on a class track their this binding in the type system; extracting one (const f = c.m) would lose that binding and is rejected with [method-unbinding] "Cannot get c.m because property m cannot be unbound from the context where it was defined." TypeScript treats methods as plain function values and lets the same extraction through silently — the resulting call then has the wrong this at runtime.
1class C {2 x: number = 0;3 m(): number { return this.x; }4}5const c = new C();6const f: () => number = c.m; // ERROR — [method-unbinding]method-unbindingCannot get c.m because property m [1] cannot be unbound from the context [2] where it was defined.The Flow rewrites are either keep the call bound (c.m() directly), wrap with an arrow that captures this (const f = () => c.m()), or call .bind (const f = c.m.bind(c)). Note this is a class-instance rule — method-shorthand on plain object types ({m(x: number): number}) doesn't carry a this context to lose (usage of this in object literals is banned), so extraction is allowed there.
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.
| Concept | TypeScript | Flow | Note |
|---|---|---|---|
Type inhabited only by undefined | undefined | void | Flow has no separate undefined type. Using undefined as an annotation errors with [unsupported-syntax]. (details) |
| "No useful value" return marker | void | void | Same 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 type | never | empty | never is the natural TS reach when Flow expects empty. |
| Top type | unknown | unknown | Same name. |
See type hierarchy for where these sit relative to the rest of Flow's types.
void vs undefined
undefined as an annotation is a hard error in Flow:
1function f(): undefined { // ERROR — [unsupported-syntax]unsupported-syntaxThe equivalent of TypeScript's undefined type in Flow is void. Flow does not have separate void and undefined types.2 return undefined;3}1function f(): void {2 return undefined; // OK — `undefined` is the value inhabiting `void`3}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. Canonical Flow forms: undefined → void 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 analogue 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.
1function f(x: ?number) {}2f(null); // OK3f(undefined); // OK4f(); // OK — `?T` includes `void`, which makes the arg optionalVariance
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.
| Surface | TypeScript | Flow | Details |
|---|---|---|---|
| Variance keywords | Uses 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 properties | Covariant. | Invariant. | Mutable object properties |
readonly property assignability | readonly 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 arrays | Covariant. | Invariant. | Mutable arrays |
| Generic type arguments | Variance is inferred from usage with compatibility-oriented exceptions. | Invariant by default unless declared out or in. | Generic type arguments |
| Method parameters | Bivariant for method syntax; function-typed fields are contravariant. | Contravariant. | Method parameters |
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) => voidis 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)
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.
1type Box<out T> = {2 set(t: T): void; // ERROR — [incompatible-variance]incompatible-varianceCannot use T [1] in an input position because T [1] is expected to occur only in output positions.3};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 leave inaccurate static types.
Mutable object properties are invariant in Flow, covariant in TS
Assigning {x: number} to {x: number | string} widens the slot's read type but also widens what can be written into it, so a downstream obj.x = "oh no" would corrupt the original.
1function f(obj: {x: number | string}) {}2const o: {x: number} = {x: 1};3f(o); // ERROR — property `x` is invariantly typedincompatible-typeCannot call f with o bound to obj because in property x: number [1] is not exactly the same as number | string [2].// TypeScript allows this — the property is covariant, so `{x: number}`
// is treated as a subtype of `{x: number | string}`.
function f(obj: {x: number | string}) {}
const o: {x: number} = {x: 1};
f(o);
The fix is to make the target read-only — either with the Readonly<T> utility or the readonly property modifier. Removing the possibility of mutation through obj is what makes the widening safe. TypeScript supports the same readonly property modifier, but see the next sub-bullet for how the two languages diverge on enforcing it.
1function f(obj: Readonly<{x: number | string}>) {} // or {readonly x: number | string}2const o: {x: number} = {x: 1};3f(o); // OKreadonly properties are interchangeable with mutable in TS, but not in Flow
Assigning a {readonly value: T} to {value: T} would let a caller drop the read-only constraint and mutate the underlying object.
1function f(obj: {value: number}) {2 obj.value = 99;3}4const o: {readonly value: number} = {value: 1};5f(o); // ERROR — [incompatible-variance]incompatible-varianceCannot call f with o bound to obj because property value is read-only in object type [1] but writable in object type [2].// TypeScript allows it; the mutation through `f` succeeds at runtime.
function f(obj: {value: number}) { obj.value = 99; }
const o: {readonly value: number} = {value: 1};
f(o);
Flow treats readonly / writeonly as load-bearing for static safety; TypeScript enforces readonly at direct write sites, but assignability can drop the readonly constraint.
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.
1function f(obj: {readonly value: number}) {}2const o: {readonly value: number} = {value: 1};3f(o); // OKMutable arrays are invariant in Flow, covariant in TS
Array<number> is not assignable to Array<number | string> in Flow — it would let an [0] = "oh no" corrupt the source.
1function f(a: Array<number | string>) {}2const xs: Array<number> = [1, 2, 3];3f(xs); // ERRORincompatible-typeCannot call f with xs bound to a because in array element: number [1] is not exactly the same as number | string [2].// TypeScript allows it.
function f(a: Array<number | string>) {}
const xs: number[] = [1, 2, 3];
f(xs);
The fix is to make the target a ReadonlyArray<T> — removing the possibility of mutation through a is what 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.
1function f(a: ReadonlyArray<number | string>) {}2const xs: Array<number> = [1, 2, 3];3f(xs); // OKGeneric type arguments are invariant by default
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.
1class C<T> { x: T; constructor(x: T) { this.x = x; } }2function f(c: C<number | string>) {}3const c: C<number> = new C(1);4f(c); // ERRORincompatible-typeCannot call f with c bound to c because in type argument T [1]: string [2] is incompatible with number [3].// TypeScript allows this.
class C<T> { x: T; constructor(x: T) { this.x = x; } }
function f(c: C<number | string>) {}
const c: C<number> = new C(1);
f(c);
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
A {compare(x: number, y: number): number} is not a subtype of {compare(x: number | string, y: number | string): number} in Flow; TypeScript treats it as one. In TypeScript, function-typed fields are contravariant but methods stay bivariant — this asymmetry is itself a TS-only wrinkle.
In Flow, both forms reject the widening, but for different reasons and with different error messages. Method shorthand fails contravariance ([incompatible-type] "the first parameter: number is incompatible with string") — 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 Wider-typed value flowing into a NumNum 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.
1type NumNum = {compare(x: number, y: number): number};2type Wider = {compare(x: number | string, y: number | string): number};3function f(w: Wider) {}4const nn: NumNum = {compare(x, y) { return x - y; }};5f(nn); // ERRORincompatible-typeCannot call f with nn bound to w because: NumNum [1] is incompatible with Wider [2] in property compare > the first parameter: number [3] is incompatible with string [4]incompatible-typeCannot call f with nn bound to w because: NumNum [1] is incompatible with Wider [2] in property compare > the second parameter: number [3] is incompatible with string [4]// TypeScript accepts this under bivariance — `nn.compare("oh", "no")`
// then attempts string subtraction at runtime.
type NumNum = {compare(x: number, y: number): number};
type Wider = {compare(x: number | string, y: number | string): number};
function f(w: Wider) {}
const nn: NumNum = {compare(x, y) { return x - y; }};
f(nn);
TypeScript's bivariance hole (accepts the unsound direction too) is invisible at the call site, which can make Flow look "stricter for no reason" if you are coming from TypeScript — except the strictness is exactly what stops nn.compare("oh", "no") from doing string subtraction at runtime.
See the variance docs and the subtyping docs for the full mechanics.
this type is restricted to output positions
The this type — used for fluent APIs and polymorphic method receivers — is constrained more tightly in Flow than in TypeScript. Output positions (return types) work in both languages: a method declared m(): this preserves the subclass type through fluent chains in Flow just as in TS — new SubBuilder().add(1).extra() keeps its SubBuilder type. Where Flow diverges is input and invariant positions. Using this as a parameter type or as a mutable field type errors with [incompatible-variance] "Cannot use this in an input position because this is expected to occur only in output positions." TypeScript accepts both freely. The rule falls out of the same variance model that makes mutable object properties and mutable arrays invariant — a writable slot typed this would let a caller stash a Builder into a SubBuilder-shaped field.
1class Builder {2 add(x: number): this { return this; } // OK — output position3 takesSelf(other: this): void {} // ERROR — input positionincompatible-varianceCannot use this [1] in an input position because this [1] is expected to occur only in output positions.4 parent: this | null = null; // ERROR — invariant fieldincompatible-varianceCannot use this [1] in an input/output position because this [1] is expected to occur only in output positions.5}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 subclass-preservation at that slot, or to make the field readonly so the position becomes covariant.
Refinement and module-level 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
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 the body has nothing to do with number, and any caller relying on this guard will be lied to:
// TypeScript accepts this.
function isNumber(x: unknown): x is number {
return typeof x === "boolean";
}
Flow validates the body of a type-guard function in both directions, and adds a separate rule about parameter writes. 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:
1function isNumber(x: unknown): x is number {2 return typeof x === "boolean"; // ERRORincompatible-type-guardCannot return (typeof x) === "boolean" because boolean [1] is incompatible with number [2].incompatible-type-guardCannot return (typeof x) === "boolean" because the negation of the predicate encoded in this expression needs to completely refine away the guard type number [1]. Consider using a one-sided type-guard (implies x is T).3}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:
1class A {}2class B extends A {}3function isA(x: unknown): x is A {4 return x instanceof B; // ERROR — negation does not refine `A` awayincompatible-type-guardCannot return x instanceof B because the negation of the predicate encoded in this expression needs to completely refine away the guard type A [1]. Consider using a one-sided type-guard (implies x is T).5}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.
Parameter writes ([function-predicate]). The refined parameter cannot be reassigned along the path to a return. A direct write triggers "at this return point it is written to":
1function isNumber(x: unknown): x is number {2 x = 1;3 return typeof x === "number"; // ERRORfunction-predicateCannot use type guard parameter x [1] because at this return point it is written to in [2].4}A write via a captured closure triggers "x is reassigned" — but only if the closure is actually called between the original parameter and the return. Defining the closure without calling it is fine:
1function isNumber(x: unknown): x is number { // ERROR — `x` is reassigned via the `reset()` call belowfunction-predicateCannot use type guard parameter x, because x [1] is reassigned in [2].2 const reset = () => { x = 1; };3 reset();4 return typeof x === "number";5}1function isNumber(x: unknown): x is number {2 const reset = () => { x = 1; }; // OK — never invoked3 return typeof x === "number";4}See the type guards docs for the full consistency rules. When only the positive direction of the predicate holds — so the negation check would (correctly) reject the guard — the Flow-only one-sided type guard form implies x is T is the intended escape hatch.
Refinement invalidation rules differ
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 have no syntactic signal. 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.
1declare function sideEffect(): void;2
3function localCase(x: ?number) {4 if (x != null) {5 sideEffect(); // bare call does NOT drop the refinement on a local6 const a: number = x; // OK7 }8}9
10function propertyCase(obj: {x: ?number}) {11 if (obj.x != null) {12 sideEffect(); // bare call DROPS the refinement on a property13 const a: number = obj.x; // ERROR — callee could have mutated `obj.x`incompatible-typeCannot assign obj.x to a because null or undefined [1] is incompatible with number [2].14 }15}16
17function writeCase(x: ?number) {18 if (x != null) {19 x = null;20 const a: number = x; // ERROR — direct write invalidates the refinementincompatible-typeCannot assign x to a because null [1] is incompatible with number [2].21 }22}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.
1declare function sideEffect(): void;2
3function propertyCaseFixed(obj: {x: ?number}) {4 const {x} = obj;5 if (x != null) {6 sideEffect();7 const a: number = x; // OK — local refinement survives the call8 }9}Annotations are required at module 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.
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.
// 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
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 onlyexport typedFooerrors[import-type-as-value]"Cannot import the typeFooas a value. Useimport typeinstead." - A value-position
export {Foo}whereFoois a type-only binding in the current module errors[type-as-value]"Cannot use typeFooas 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.
// mod.js
export type Foo = {x: number};
// consumer.js
import {Foo} from './mod'; // ERROR — [import-type-as-value]
import type {Foo as FooType} from './mod'; // OK
Explicit type 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 stricter in Flow
Flow's as only widens or asserts (e.g. 42 as number, 42 as 42), and rejects unsafe downcasts at the type level — {foo: 1} as {foo: number, bar: 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 bar: string property that doesn't exist at runtime. This permissiveness is the single biggest source of "TS code that looks like it should work in Flow but doesn't."
1const v = {foo: 1} as {foo: number, bar: string}; // ERRORincompatible-typeCannot cast object literal to object type because property bar is missing in object literal [1] but exists in object type [2].// TypeScript accepts the same line because `as` permits assignability
// in either direction.
const v = {foo: 1} as {foo: number, bar: string};
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
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 errors as unused 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 auditable. See the errors docs.
1declare function takesNumber(n: number): void;2// $FlowFixMe[incompatible-type] - intentional for demo3takesNumber("not a number");Generic type arguments cannot be omitted
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."
1type Foo<T = string> = {x: T};2type A = Foo; // ERROR — [missing-type-arg]missing-type-argCannot use Foo [1] without 0-1 type arguments.3type B = Foo<>; // OK — uses the default `T = string`The rewrite is mechanical: Foo → Foo<> 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 — type-level functions and the like — 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 analogue
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 analogue to translate from, only a Flow concept to learn fresh.
component syntax
Flow ships first-class component syntax for declaring React components with named props, optional ref, and render-type support. The compiler enforces rules that TypeScript's function-type component model does not encode as syntax — return type fixed to React.Node, no this, no nested components, ref parameters in their dedicated position. There is no built-in TypeScript equivalent; TypeScript models the equivalent shape with function types and forwardRef.
1import * as React from 'react';2
3component Greeting(name: string, age?: number) {4 return <div>Hello, {name}{age != null ? `, age ${age}` : ''}</div>;5}6
7const _ = <Greeting name="Alice" age={30} />;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.
1import {useState} from 'react';2
3hook useToggle(initial: boolean): [boolean, () => void] {4 const [value, setValue] = useState(initial);5 return [value, () => setValue(v => !v)];6}renders types
Render types (renders, renders?, renders*) constrain what a component is allowed to render — for example, "a Menu only renders MenuItems." There is no built-in TypeScript analogue.
1import * as React from 'react';2
3component MenuItem() {4 return <li />;5}6
7component Menu(children: renders MenuItem) {8 return <ul>{children}</ul>;9}10
11const _ = <Menu><MenuItem /></Menu>;match expressions and statements
Flow has match expressions and statements for pattern matching with structural patterns, guards, and exhaustiveness checking. TypeScript has no match; the closest analogue for the statement form is a hand-coded discriminated-union switch with an assertNever fallthrough. The expression form has no direct TS analogue 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.
1type Shape =2 | {kind: 'circle', radius: number}3 | {kind: 'square', side: number};4
5declare const s: Shape;6
7const area: number = match (s) {8 {kind: 'circle', const radius} => Math.PI * radius * radius,9 {kind: 'square', const side} => side * side,10};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 brand idiom enforces is weaker than Flow's file-scoped abstraction along two axes. First, a single as cast is enough to forge a branded value — "abc" as UserId type-checks in TypeScript because the source (string) and the target (string & {readonly [brand]: true}) overlap on string, and TS only rejects an as cast when the two sides are disjoint. (The as unknown as T double-cast is the universal escape hatch.) Second, when the brand key is exposed (re-exported unique symbol, or a plain string key like __brand: "UserId"), any consumer can structurally construct a branded value directly, no cast required. Even with an unexported unique symbol, the as route remains open. 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 neither structural construction nor as widening can produce the opaque type from its underlying representation.
1opaque type UserId = number;2
3declare function makeUserId(n: number): UserId;4declare function lookupUser(id: UserId): string;5
6const id: UserId = makeUserId(42);7lookupUser(id); // OK8// In another file, `42` is not a `UserId` and a `UserId` is not a `number`.9// Inside this file (where the underlying type is visible) the conversion is allowed:10const n: number = id;Flow Enums
Flow Enums and TypeScript enum look superficially similar but are very different in detail.
| Aspect | TypeScript | Flow |
|---|---|---|
Exhaustive switch | No built-in diagnostic; encoded with never or lint. | Built-in: [invalid-exhaustive-check] if a member is forgotten. |
| Implicit coercion to/from underlying primitive | Permits 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 values | Number enums auto-number from 0. | Number-enum members must be explicitly initialized ([invalid-enum]); string enums default to mirroring member names. |
| Re-declaration | Allowed; can collide with default values silently. | [name-already-bound]. |
| Reverse mapping | Number enums get a runtime reverse-map; string enums error on the same access. | .getName(value) works for both number and string enums. |
| Iterating members | for...in over a number enum produces both numeric keys and member names. | Status.members() returns just the values. |
| Symbol enums | None. | Supported (enum X of symbol { ... }). |
| Definition restrictions | Permits 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.Offhas literal type"off"(the value), not"Off"(the key), soStatusStr[StatusStr.Off]resolves to a non-existentStatusStr["off"]. - TS
for...inover a 3-member number enum produces[ '0', '1', '2', 'Active', 'Paused', 'Off' ]— both halves of the runtime reverse-map are enumerable. - Lowercase-leading member names are reserved because Flow Enums expose lowercase methods like
.castand.members.
1enum Status {2 Active,3 Paused,4 Off,5}6
7declare const st: Status;8
9let label: string;10switch (st) {11 case Status.Active: label = 'on'; break;12 case Status.Paused: label = 'wait'; break;13 case Status.Off: label = 'off'; break;14 // Exhaustive — removing a case here errors.15}See the Flow Enums docs for full mechanics.
One-sided type guards (implies)
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.
1declare function looksLikeFoo(x: unknown): implies x is {foo: string, ...};2
3declare const v: unknown;4if (looksLikeFoo(v)) {5 v.foo as string; // refined to {foo: string, ...}6}7// In the else branch, `v` stays `unknown` — that's the "one-sided" property.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 analogue 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.
1// `import type` imports a *type* declaration — `Node` is a type export, so2// it can be used directly as a type annotation:3import type {Node as ReactNode} from 'react';4const node: ReactNode = "hello, world";5
6// `import typeof` imports the *type of a value*. `useState` is a value, but7// after `import typeof` the name `useState` is usable as a type. Generic8// values stay generic — `useState<number>` instantiates the underlying9// function type to its `number` specialization, so the parameter below is10// callable as a `number`-typed `useState`:11import typeof {useState} from 'react';12hook useCounter(useStateNum: useState<number>): number {13 const [count, setCount] = useStateNum(0);14 setCount(c => c + 1);15 return count;16}Flow-only utility types
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 typeT. No TS native form; the usual TS encoding isnew (...args: any[]) => Tortypeof Tfor a specific class.Values<T>— the union of value types ofT's properties. TS spelling is the indexed accessT[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 analogue istypeof import('mod'), with a different shape.StringPrefix<P>/StringSuffix<S>— strings constrained to a literal prefix or suffix. The TS analogue 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
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
interfacetype annotation —type T = interface { foo: number }. Lets an interface appear inside a type expression instead of as a top-level declaration. TypeScript requires a separateinterface I { ... }statement. - Optional indexed access type —
Obj?.['prop']mirrors the runtime?.operator at the type level: ifObjis nullish, the result isvoid; otherwise it isObj['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}.
1type Inline = interface { foo: number };2type Opt = ?{foo: number};3type Pulled = Opt?.['foo']; // number | void4type Fn = string => void;5type Dict = {[string]: number};Relay / GraphQL 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.
TypeScript-only features that do not exist in Flow
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 that are in-flight. 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
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 [unsupported-syntax] diagnostic that points at the Flow rewrite directly.
- Angle-bracket type assertion — TS
<T>x→ Flowx as T. - Optional unlabeled tuple elements — TS
[number, string?]→ Flow[a: number, b?: string]. Flow requires the labeled variant for optional elements. readonlytype operator on tuples — TSreadonly [T, S]→ FlowReadonly<[T, S]>.readonlytype operator on array shorthand — TSreadonly T[]→ FlowReadonlyArray<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.
1type A = readonly [number, string]; // ERROR — use Readonly<[number, string]>unsupported-syntaxThe equivalent of TypeScript's readonly type operator applied to a tuple type is Readonly<[T, S]>.2type B = readonly number[]; // ERROR — use ReadonlyArray<number>unsupported-syntaxThe equivalent of TypeScript's readonly type operator applied to an array type is ReadonlyArray<T>.3type C = [number, string?]; // ERROR — use the labeled form belowunsupported-syntaxOptional unlabeled tuple element is not enabled.4type OkA = Readonly<[number, string]>;5type OkB = ReadonlyArray<number>;6type OkC = [a: number, b?: string];Decorators
Flow has no decorator support in any mode. TypeScript supports two incompatible modes: stage-3 decorators (the default, with a context-object parameter) and legacy decorators (under --experimentalDecorators, with the old (target, key) signature).
TypeScript class syntax 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."
1class C {2 constructor(public x: number) {} // ERROR — [unsupported-syntax]unsupported-syntaxFlow does not support TypeScript parameter properties. To fix, declare the property in the class body and assign it in the constructor.3}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.
1class C {2 public a: number = 1; // ERROR — drop the modifierunsupported-syntaxFlow does not support using public in classes. Fields and methods are public by default. To fix, remove the public modifier.3 protected b: number = 2; // ERROR — drop the modifierunsupported-syntaxFlow does not support using protected in classes. To fix, remove the protected modifier.4 private c: number = 3; // ERROR — use `#c` insteadunsupported-syntaxFlow does not support using private in classes. Use JavaScript private elements instead. To fix, change private foo to #foo.5}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. Flow tracks #private nominally as part of the class identity.
accessor auto-accessors (class C { accessor x: T = init }) — a stage-3 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
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
No equivalent. Flow Enums are a runtime construct by design and do not have an inlined-at-compile-time mode. However, their restrictions (literal-only values, no redeclaration, no default number values) make it easier for your build system to do inlining.
infer extends
No equivalent. Flow's infer exists in conditional types but does not support TypeScript's infer T extends Bound constraint form. Restructure the conditional or move the bound check elsewhere.
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 assertion-function syntax errors with [unsupported-syntax] "Type guard assertions are not yet supported."
1function specificAssert(arg: unknown): asserts arg is string { // ERROR — [unsupported-syntax]unsupported-syntaxType guard assertions are not yet supported.2 if (typeof arg !== 'string') {3 throw new Error();4 }5}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();.
Expressions with type arguments
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 at the closing > with a ParseError.
// 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
TypeScript narrows destructured properties of a discriminated union together: refining the sentinel binding also refines the other bindings extracted in the same destructuring.
// TypeScript:
type Shape =
| {kind: 'circle', value: number}
| {kind: 'square', value: string};
declare const s: Shape;
const {kind, value} = s;
if (kind === 'circle') {
const r: number = value; // OK — TS narrows `value` based on `kind`
} else {
const sd: string = value;
}
Flow refines sentinel-tagged unions through the original value (if (s.kind === 'circle') { ... s.value ... }), but each destructured binding carries its full union type independent of the others, so the same code fails:
1type Shape =2 | {kind: 'circle', value: number}3 | {kind: 'square', value: string};4
5declare const s: Shape;6const {kind, value} = s;7if (kind === 'circle') {8 const r: number = value; // ERROR — `value` keeps its full `number | string` typeincompatible-typeCannot assign value to r because string [1] is incompatible with number [2].9}The Flow rewrite is to refine through the original value rather than destructure.
User-side 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
TypeScript features that are in-progress on the Flow side and will be released in the future. Each of these is TS-only for now; the entries in TypeScript-only features above are the ones Flow has no in-flight plans to add.
satisfiesexpression — validates an expression against a type without widening the inferred type.- Mapped type modifiers — optionality removal
-?, variance removal-readonly, andaskey remapping. - Template literal types — e.g.
`${'a' | 'b'}-${'x' | 'y'}`. - Additional TS utility types —
ConstructorParameters,InstanceType,ThisType, and the intrinsic string-manipulation typesUppercase,Lowercase,Capitalize,Uncapitalize. overridekeyword on class members.- Abstract classes and methods.
- Constructor types —
type Ctor = new (x: number) => R. - Symbol-keyed properties and
unique symbol. - Inline
import()type expression —type A = import('./m').A. import X = require('foo')andexport = X— CommonJS-style import and export bindings.
Syntax convergence with TypeScript
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 Flow | Modern Flow (TS-aligned) |
|---|---|
mixed | unknown |
$Keys<T> | keyof T |
$ReadOnly<T> | Readonly<T> |
$NonMaybeType<T> | NonNullable<T> |
$ReadOnlyArray<T> | ReadonlyArray<T> |
<T: Bound> | <T extends Bound> |
(x: T) cast | x as T |
{| a: number |} exact | {a: number} (exact is the default) |
+foo / -foo property variance | readonly foo / writeonly foo (writeonly is Flow-specific) |
+T / -T type parameter variance | out T / in T |
%checks predicate functions | user-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
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 option | Flow option | Description |
|---|---|---|
noUncheckedIndexedAccess | no_unchecked_indexed_access | Indexed 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. |
useUnknownInCatchVariables | use_unknown_in_catch_variables | Changes 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
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 case | Flow mechanism | Placement 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
Underpinning the merging cases below: Flow uses TypeScript's split-namespace model. 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, opaque types — register once in the value namespace and the type side falls back to it.
On top of that namespace model, Flow supports the merging cases that matter most. Specifically, Flow merges:
interface+interface— members union (first-wins on conflicts),extendslists concatenate, call signatures overload as intersections, type-param arities must match. Supported in both type-sig and local checking.declare module+declare module— exports union (first-wins on name collisions), incompatible export styles (CJS vs. ES) error, star re-exports concatenate.declare class+interface— interface members fold into the class (either order).function/declare function+declare namespace— namespace's type members fold into the function (either order).class/declare class+declare namespace— namespace's type members fold into the class (either order).
What Flow does not do is the runtime-merging cases — for example, arbitrary function + namespace value-side merging where the namespace contributes runtime members, and the user-side declare module 'name' { ... } source-level augmentation pattern.
Generating declaration files
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
- 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.