Skip to main content

Classes

JavaScript classes in Flow operate both as a value and a type. You can use the name of the class as the type of its instances:

1class MyClass {2  // ...3}4
5const myInstance: MyClass = new MyClass(); // Works!

This is because classes in Flow are nominally typed.

This means two classes with identical shapes are not compatible:

1class A {2  x: number;3}4class B {5  x: number;6}7const foo: B = new A(); // Error!incompatible-typeCannot assign new A() to foo because A [1] is incompatible with B [2].8const bar: A = new B(); // Error!incompatible-typeCannot assign new B() to bar because B [1] is incompatible with A [2].

You also cannot use an object type to describe an instance of a class — Flow reports this as [class-object-subtyping] ("Class instances are not subtypes of object types; consider rewriting object type as an interface"):

1class MyClass {2  x: number;3}4const foo: {x: number, ...} = new MyClass(); // Error!class-object-subtypingCannot assign new MyClass() to foo because MyClass [1] is not a subtype of object type [2]. Class instances are not subtypes of object types; consider rewriting object type [2] as an interface.

You can use interfaces to accomplish this instead — interfaces accept both plain objects and class instances:

1class A {2  x: number;3}4class B {5  x: number;6}7
8interface WithXNum {9  x: number;10}11
12const foo: WithXNum = new A(); // Works!13const bar: WithXNum = new B(); // Works!14
15const n: number = foo.x; // Works!
TypeScript comparison

TypeScript types classes structurally, while Flow types them nominally — two distinct classes with the same shape are different types. Flow also rejects method unbinding (const f = c.m) because the extracted method would lose its this, while TS lets the same extraction through silently. And several TS-only class syntax extensions — parameter properties, access modifiers — are not adopted in Flow, write the equivalent JS instead.

When to use this

Use classes when you need methods, inheritance, or nominal typing — two classes with the same shape are distinct types. When you only need to describe data shape, use object types. When you need structural compatibility across classes, use interfaces.

Class Syntax

Classes in Flow are just like normal JavaScript classes, but with added types.

Class Methods

Just like in functions, class methods can have annotations for both parameters (input) and returns (output):

1class MyClass {2  method(value: string): number {3    return 0;4  }5}

Also just like regular functions, class methods may have this annotations as well. However, if one is not provided, Flow will infer the class instance type (or the class type for static methods) instead of mixed. When an explicit this parameter is provided, it must be a supertype of the class instance type (or class type for static methods).

1class MyClass {2  method(this: interface {x: string}) { /* ... */ } // Error!incompatible-typeCannot define method method [1] on MyClass because property x is missing in MyClass [2] but exists in interface type [3].3}

The this type can also appear in a method signature — typically as a return type (add(x: T): this) for fluent APIs that preserve the subclass type through chained calls. Flow allows this only in covariant positions (return types and readonly fields); see The this type is restricted to covariant positions for the input/mutable-field rules and rewrites.

Methods are considered read-only:

1class MyClass {2  method() {}3}4
5const a = new MyClass();6a.method = function() {}; // Error!cannot-writeCannot assign function to a.method because property method is not writable.

Flow supports private methods, a feature of ES2022. Private methods start with a hash symbol #:

1class MyClass {2  #internalMethod() {3    return 1;4  }5  publicApi() {6    return this.#internalMethod();7  }8}9
10const a = new MyClass();11a.#internalMethod(); // Error!Private fields can only be referenced from within a class. [ParseError]12a.publicApi(); // Works!

Flow requires return type annotations on methods in most cases. This is because it is common to reference this inside of a method, and this is typed as the instance of the class - but to know the type of the class we need to know the return type of its methods!

1class MyClass {2  foo() { // Error!missing-local-annotMissing an annotation on return.3    return this.bar();4  }5  bar() { // Error!missing-local-annotMissing an annotation on return.6    return 1;7  }8}
1class MyClassFixed {2  foo(): number { // Works!3    return this.bar();4  }5  bar(): number { // Works!6    return 1;7  }8}

Class Fields (Properties)

Whenever you want to use a class field in Flow you must first give it an annotation:

1class MyClass {2  method() {3    this.prop = 42; // Error!prop-missingCannot assign 42 to this.prop because property prop is missing in MyClass [1].4  }5}

Fields are annotated within the body of the class with the field name followed by a colon : and the type:

1class MyClass {2  prop: number;3  method() {4    this.prop = 42;5  }6}

Fields added outside of the class definition need to be annotated within the body of the class:

1function func(x: number): number {2  return x + 1;3}4
5class MyClass {6  static constant: number;7  static helper: (number) => number;8  prop: number => number;9}10MyClass.helper = func11MyClass.constant = 4212MyClass.prototype.prop = func

Flow also supports using the class properties syntax:

1class MyClass {2  prop = 42;3}

When using this syntax, you are not required to give it a type annotation. But you still can if you need to:

1class MyClass {2  prop: number = 42;3}

You can mark a class field as read-only (or write-only) using variance annotations. These can only be written to in the constructor:

1class MyClass {2  readonly prop: number;3
4  constructor() {5    this.prop = 1; // Works!6  }7
8  method() {9    this.prop = 1; // Error!cannot-writeCannot assign 1 to this.prop because property prop is not writable.10  }11}12
13const a = new MyClass();14const n: number = a.prop; // Works!15a.prop = 1; // Error!cannot-writeCannot assign 1 to a.prop because property prop is not writable.

Flow supports private fields, a feature of ES2022. Private fields start with a hash symbol #:

1class MyClass {2  #internalValue: number;3
4  constructor() {5    this.#internalValue = 1;6  }7
8  publicApi() {9    return this.#internalValue;10  }11}12
13const a = new MyClass();14const x: number = a.#internalValue; // Error!Private fields can only be referenced from within a class. [ParseError]15const y: number = a.publicApi(); // Works!

Extending classes and implementing interfaces

You can optionally extend one other class:

1class Base {2  x: number;3}4
5class MyClass extends Base {6  y: string;7}

And also implement multiple interfaces:

1interface WithXNum {2  x: number;3}4interface Readable {5  read(): string;6}7
8class MyClass implements WithXNum, Readable {9  x: number;10  read(): string {11    return String(this.x);12  }13}

You don't need to implement an interface to be a subtype of it, but doing so enforces that your class meets the requirements:

1interface WithXNum {2  x: number;3}4
5class MyClass implements WithXNum { // Error!incompatible-typeCannot implement WithXNum [1] with MyClass because property x is missing in MyClass [2] but exists in WithXNum [1].6}

The right-hand side of implements and extends is constrained — see Common Issues below.

Class Constructors

You can initialize your class properties in class constructors:

1class MyClass {2  foo: number;3
4  constructor() {5    this.foo = 1;6  }7}

You must first call super(...) in a derived class before you can access this and super:

1class Base {2  bar: number;3}4
5class MyClass extends Base {6  foo: number;7
8  constructor() {9    this.foo; // Errorreference-before-declarationMust call super before accessing this [1] in a derived constructor.10    this.bar; // Errorreference-before-declarationMust call super before accessing this [1] in a derived constructor.11    super.bar; // Errorreference-before-declarationMust call super before accessing super [1] in a derived constructor.12    super();13    this.foo; // OK14    this.bar; // OK15    super.bar; // OK16  }17}

However, Flow will not enforce that all class properties are initialized in constructors:

1class MyClass {2  foo: number;3  bar: number;4
5  constructor() {6    this.foo = 1;7  }8
9  useBar() {10    this.bar as number; // No errors.11  }12}

Class Generics

Classes can also have their own generics:

1class MyClass<A, B, C> {2  property: A;3  method(val: B): C {4    throw new Error();5  }6}

Class generics are parameterized. When you use a class as a type you need to pass parameters for each of its generics:

1class MyClass<A, B, C> {2  constructor(arg1: A, arg2: B, arg3: C) {3    // ...4  }5}6
7const val: MyClass<number, boolean, string> = new MyClass(1, true, 'three');

Classes in annotations

When you use the name of your class in an annotation, it means an instance of your class:

class MyClass {}

const b: MyClass = new MyClass(); // Works!
const a: MyClass = MyClass; // Error!

See here for details on Class<T>, which allows you to refer to the type of the class in an annotation.

Common Issues

Method unbinding

Flow tracks the this binding on methods: extracting obj.method without calling it would produce a function that has lost its this, and calling that function would invoke the method body with this undefined, so any this.field access would crash at runtime. Flow rejects the extraction with [method-unbinding] ("Cannot get a.method because property method cannot be unbound from the context where it was defined"):

1class Counter {2  count: number = 0;3  increment(): number { return ++this.count; }4}5const counter = new Counter();6const tick: () => number = counter.increment; // ERROR: [method-unbinding]method-unbindingCannot get counter.increment because property increment [1] cannot be unbound from the context [2] where it was defined.

Destructuring is blocked for the same reason:

1class MyClass { method() {} }2const a = new MyClass();3const {method} = a; // Error!method-unbindingproperty method [1] cannot be unbound from the context [2] where it was defined.

The fixes are either to keep the call bound (counter.increment() directly) or to wrap with an arrow that captures this (const tick = () => counter.increment()).

This is a class-instance rule: method-shorthand on a plain object type doesn't carry a this context to lose (usage of this in object literals is banned), so extracting an object method is allowed.

implements and extends RHS must be an interface or class

The right-hand side of implements (on a class) or extends (on an interface) must name an interface or class — passing an object-type alias errors:

  • class C implements ObjType errors with [cannot-implement] ("…is not an interface").
  • interface I extends ObjType errors with [incompatible-use] ("…is not inheritable").

Mapped or utility types applied to interfaces produce object types, which also won't be accepted in either clause. The fix is to introduce a named interface (or inline the members directly) instead.

See Also

  • Interfaces — structural typing for classes, allowing different classes to be used interchangeably
  • Nominal & Structural Typing — why classes are compared by name, not shape
  • Generics — parameterized types, used with classes, functions, and type aliases
  • Variance — controlling read-only and write-only properties
  • Utility TypesClass<T> for referring to the class type itself (not instances)