Классы
Классы 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 классы типизируются структурно, а в 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