Spec-Zone.ru › Flow

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:

class MyClass {
  // ...
}


const myInstance: MyClass = new MyClass(); // Works!

This is because classes in Flow are nominally typed.

This means two classes with identical shapes are not compatible:

class A {
  x: number;
}
class B {
  x: number;
}
const foo: B = new A(); // Error!
const bar: A = new B(); // Error!

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"):

class MyClass {
  x: number;
}
const foo: {x: number, ...} = new MyClass(); // Error!

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

class A {
  x: number;
}
class B {
  x: number;
}


interface WithXNum {
  x: number;
}


const foo: WithXNum = new A(); // Works!
const bar: WithXNum = new B(); // Works!


const 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):

class MyClass {
  method(value: string): number {
    return 0;
  }
}

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).

class MyClass {
  method(this: interface {x: string}) { /* ... */ } // Error!
}

The this type can also appear in a method signature — typically as a return type (add(x: number): 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:

class MyClass {
  method() {}
}


const a = new MyClass();
a.method = function() {}; // Error!

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

class MyClass {
  #internalMethod() {
    return 1;
  }
  publicApi() {
    return this.#internalMethod();
  }
}


const a = new MyClass();
a.#internalMethod(); // Error!
a.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!

class MyClass {
  foo() { // Error!
    return this.bar();
  }
  bar() { // Error!
    return 1;
  }
}
class MyClassFixed {
  foo(): number { // Works!
    return this.bar();
  }
  bar(): number { // Works!
    return 1;
  }
}

Class Fields (Properties)​

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

class MyClass {
  method() {
    this.prop = 42; // Error!
  }
}

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

class MyClass {
  prop: number;
  method() {
    this.prop = 42;
  }
}

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

function func(x: number): number {
  return x + 1;
}


class MyClass {
  static constant: number;
  static helper: (x: number) => number;
  prop: (x: number) => number;
}
MyClass.helper = func
MyClass.constant = 42
MyClass.prototype.prop = func

Flow also supports using the class properties syntax:

class MyClass {
  prop = 42;
}

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

class MyClass {
  prop: number = 42;
}

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

class MyClass {
  readonly prop: number;


  constructor() {
    this.prop = 1; // Works!
  }


  method() {
    this.prop = 1; // Error!
  }
}


const a = new MyClass();
const n: number = a.prop; // Works!
a.prop = 1; // Error!

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

class MyClass {
  #internalValue: number;


  constructor() {
    this.#internalValue = 1;
  }


  publicApi() {
    return this.#internalValue;
  }
}


const a = new MyClass();
const x: number = a.#internalValue; // Error!
const y: number = a.publicApi(); // Works!

Extending classes and implementing interfaces​

You can optionally extend one other class:

class Base {
  x: number;
}


class MyClass extends Base {
  y: string;
}

And also implement multiple interfaces:

interface WithXNum {
  x: number;
}
interface Readable {
  read(): string;
}


class MyClass implements WithXNum, Readable {
  x: number;
  read(): string {
    return String(this.x);
  }
}

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

interface WithXNum {
  x: number;
}


class MyClass implements WithXNum { // Error!
}

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:

class MyClass {
  foo: number;


  constructor() {
    this.foo = 1;
  }
}

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

class Base {
  bar: number;
}


class MyClass extends Base {
  foo: number;


  constructor() {
    this.foo; // Error
    this.bar; // Error
    super.bar; // Error
    super();
    this.foo; // OK
    this.bar; // OK
    super.bar; // OK
  }
}

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

class MyClass {
  foo: number;
  bar: number;


  constructor() {
    this.foo = 1;
  }


  useBar() {
    this.bar as number; // No errors.
  }
}

Class Generics​

Classes can also have their own generics:

class MyClass<A, B, C> {
  property: A;
  method(val: B): C {
    throw new Error();
  }
}

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

class MyClass<A, B, C> {
  constructor(arg1: A, arg2: B, arg3: C) {
    // ...
  }
}


const 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"):

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

Destructuring is blocked for the same reason:

class MyClass { method() {} }
const a = new MyClass();
const {method} = a; // Error!

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.

Rebinding this with call, apply and bind​

call, apply and bind cannot safely accept a receiver merely because its type is compatible. A subclass method can rely on a more specific this than is visible at the call site:

class X {
  x: string = 'x';
  foo(this: X): string { return this.x; }
}

class Y extends X {
  y: string = 'y';
  foo(this: Y): string { return this.y; }
}

const x: X = new Y(); // Fine: Y is a subtype of X
x.foo.call(new X()); // Would run Y.prototype.foo with an X as this

At runtime, x.foo is Y.prototype.foo, so rebinding it to new X() would evaluate this.y on an object with no y. Flow therefore only lets call, apply and bind re-supply the receiver from which the method was read. The class-instance example is rejected earlier by method unbinding; in cases where the method read itself is valid, a different receiver reports invalid-this-arg:

type Counter = {
  count: number,
  increment(this: Counter): number,
};


declare const counter: Counter;
declare const other: Counter;


counter.increment.call(counter); // Works
counter.increment.call(other); // ERROR: [invalid-this-arg]

invalid-this-arg is a lint that is enabled as an error by default; // flowlint invalid-this-arg:off turns it off for a region of a file.

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 Types — Class<T> for referring to the class type itself (not instances)

© 2013–present Facebook Inc.
Licensed under the MIT License.
https://flow.org/en/docs/types/classes

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API