Spec-Zone.ru › Flow

Классы

Классы JavaScript в Flow работают и как значение, и как тип. Имя класса можно использовать в качестве типа его экземпляров:

class MyClass {
  // ...
}


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

Это связано с тем, что классы в Flow имеют номинальную типизацию.

Это означает, что два класса с одинаковой структурой несовместимы:

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

Также нельзя использовать тип объекта, чтобы описать экземпляр класса — Flow сообщает об этом как об ошибке [class-object-subtyping] («Экземпляры классов не являются подтипами типов объектов; попробуйте переписать тип объекта как интерфейс»):

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

Вместо этого можно использовать интерфейсы — интерфейсы подходят и для обычных объектов, и для экземпляров классов:

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

В TypeScript классы типизируются структурно, а в Flow — номинально: два разных класса с одинаковой структурой являются разными типами. Flow также запрещает отвязку методов (const f = c.m), поскольку извлечённый метод потеряет свой this, тогда как TS молча разрешает такое извлечение. Кроме того, некоторые расширения синтаксиса классов, доступные только в TS, — свойства параметров и модификаторы доступа — в Flow не используются; вместо них пишите эквивалентный код JS.

Когда это использовать​

Используйте классы, когда нужны методы, наследование или номинальная типизация — два класса с одинаковой структурой являются разными типами. Если нужно только описать структуру данных, используйте типы объектов. Если нужна структурная совместимость между классами, используйте интерфейсы.

Синтаксис классов​

Классы в Flow такие же, как обычные классы JavaScript, но с добавленными типами.

Методы классов​

Как и в функциях, для параметров (входных данных) и возвращаемых значений (выходных данных) методов класса можно указывать аннотации:

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

Как и для обычных функций, для методов класса также можно указывать аннотации this. Однако если такая аннотация не указана, Flow выведет тип экземпляра класса (или тип класса для статических методов), а не mixed. Если явно указан параметр this, он должен быть супертипом типа экземпляра класса (или типа класса для статических методов).

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

Тип this также может встречаться в сигнатуре метода — обычно в качестве возвращаемого типа (add(x: number): this) во fluent API, где при цепочке вызовов сохраняется тип подкласса. Flow разрешает использовать this только в ковариантных позициях (возвращаемые типы и поля readonly); правила и преобразования для входных данных и изменяемых полей см. в разделе Тип this допускается только в ковариантных позициях.

Методы считаются доступными только для чтения:

class MyClass {
  method() {}
}


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

Flow поддерживает приватные методы — возможность из ES2022. Имена приватных методов начинаются с символа решётки #:

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


const a = new MyClass();
a.#internalMethod(); // Error!
a.publicApi(); // Works!

В большинстве случаев Flow требует аннотации возвращаемого типа для методов. Это связано с тем, что внутри метода часто используется this, а this типизируется как экземпляр класса. Но чтобы узнать тип класса, нам нужно знать возвращаемые типы его методов!

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

Поля класса (свойства)​

При использовании поля класса в Flow сначала необходимо снабдить его аннотацией:

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

Аннотации полей указываются в теле класса: после имени поля ставится двоеточие :, а затем указывается тип:

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

Поля, добавленные за пределами определения класса, необходимо аннотировать в теле класса:

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 также поддерживает синтаксис свойств класса:

class MyClass {
  prop = 42;
}

При использовании этого синтаксиса аннотацию типа указывать необязательно. Но при необходимости её всё же можно добавить:

class MyClass {
  prop: number = 42;
}

Можно пометить поле класса как доступное только для чтения (или только для записи) с помощью аннотаций вариантности. Записывать в такие поля можно только в конструкторе:

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 поддерживает приватные поля — возможность из ES2022. Имена приватных полей начинаются с символа решётки #:

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!

Наследование классов и реализация интерфейсов​

При необходимости можно extend один другой класс:

class Base {
  x: number;
}


class MyClass extends Base {
  y: string;
}

А также реализовать несколько интерфейсов:

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


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

Чтобы класс был подтипом интерфейса, необязательно implement его, но это позволяет проверить, что класс соответствует требованиям:

interface WithXNum {
  x: number;
}


class MyClass implements WithXNum { // Error!
}

Правая часть implements и extends ограничена — см. раздел «Распространённые проблемы» ниже.

Конструкторы классов​

Инициализировать свойства класса можно в его конструкторах:

class MyClass {
  foo: number;


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

В производном классе сначала нужно вызвать super(...), прежде чем обращаться к this и 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
  }
}

Однако Flow не проверяет, инициализированы ли все свойства класса в конструкторах:

class MyClass {
  foo: number;
  bar: number;


  constructor() {
    this.foo = 1;
  }


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

Обобщённые типы классов​

У классов также могут быть собственные обобщённые типы:

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

Обобщённые типы классов являются параметризованными. При использовании класса в качестве типа необходимо передать параметры для каждого его обобщённого типа:

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


const val: MyClass<number, boolean, string> = new MyClass(1, true, 'three');

Классы в аннотациях​

Имя класса в аннотации обозначает экземпляр этого класса:

class MyClass {}


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

Подробнее о Class<T>, позволяющем ссылаться в аннотации на тип самого класса, см. здесь.

Распространённые проблемы​

Отвязка методов​

Flow отслеживает привязку this у методов: извлечение obj.method без вызова создаст функцию, потерявшую свой this, и вызов этой функции выполнит тело метода с неопределённым this, поэтому любое обращение к this.field приведёт к сбою во время выполнения. Flow запрещает извлечение и сообщает ошибку [method-unbinding] («Невозможно получить a.method, поскольку свойство method нельзя отвязать от контекста, в котором оно было определено»):

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

Деструктуризация запрещена по той же причине:

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

Есть два способа исправить это: сохранить привязку при вызове (counter.increment() напрямую) или обернуть вызов в стрелочную функцию, захватывающую this (const tick = () => counter.increment()).

Это правило относится к экземплярам классов: сокращённая запись метода в обычном типе объекта не связана с контекстом this, который можно потерять (использование this в литералах объектов запрещено), поэтому извлечение метода объекта разрешено.

Повторная привязка this с помощью call, apply и bind​

call, apply и bind не могут безопасно принимать объект-получатель только потому, что его тип совместим. Метод подкласса может полагаться на более конкретный this, чем тот, который известен в месте вызова:

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

Во время выполнения x.foo равен Y.prototype.foo, поэтому повторная привязка к new X() приведёт к вычислению this.y для объекта без y. Поэтому Flow разрешает call, apply и bind повторно передавать только тот объект-получатель, у которого был прочитан метод. Пример с экземпляром класса отклоняется раньше из-за отвязки метода; если же чтение метода само по себе допустимо, использование другого объекта-получателя приводит к ошибке 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 — это правило линтера, которое по умолчанию включено как ошибка; // flowlint invalid-this-arg:off отключает его для части файла.

Правая часть implements и extends должна быть интерфейсом или классом​

В правой части implements (для класса) или extends (для интерфейса) должно быть указано имя интерфейса или класса — при передаче псевдонима типа объекта возникает ошибка:

  • class C implements ObjType приводит к ошибке [cannot-implement] («…не является интерфейсом»).
  • interface I extends ObjType приводит к ошибке [incompatible-use] («…не допускает наследования»).

Типы отображения или утилитарные типы, применённые к интерфейсам, создают типы объектов, которые также не допускаются ни в одной из этих конструкций. Чтобы исправить это, объявите именованный интерфейс (или укажите его члены непосредственно).

См. также​

  • Интерфейсы — структурная типизация классов, позволяющая взаимозаменяемо использовать разные классы
  • Номинальная и структурная типизация — почему классы сравниваются по именам, а не по структуре
  • Обобщённые типы — параметризованные типы, используемые с классами, функциями и псевдонимами типов
  • Вариантность — управление свойствами, доступными только для чтения или только для записи
  • Утилитарные типы — Class<T> для ссылки на тип самого класса (а не на его экземпляры)

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

Spec-Zone.ru

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