Spec-Zone.ru › Flow

Интерфейсы

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

interface Serializable {
  serialize(): string;
}


class Foo {
  serialize(): string { return '[Foo]'; }
}


class Bar {
  serialize(): string { return '[Bar]'; }
}


const foo: Foo = new Bar(); // Error!


const foo2: Serializable = new Foo(); // Works!
const bar2: Serializable = new Bar(); // Works!
Сравнение с TypeScript

В TypeScript классы типизируются структурно, поэтому интерфейсы и типы объектов в значительной степени взаимозаменяемы. Flow отличается в трёх аспектах: интерфейс не является подтипом типа объекта, поскольку интерфейсы могут быть реализованы классами, которые Flow типизирует номинально; implements и extends должны указывать на интерфейс или класс, а не на произвольный тип объекта; и примитивы не являются подтипами интерфейсов или типов объектов в Flow, тогда как TS позволяет string соответствовать {length: number}.

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

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

Интерфейсы для экземпляров и объектов​

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

class Foo {
  a: number;
}
const foo = new Foo();
const o: {a: number} = {a: 1};


interface MyInterface {
  a: number;
}


function acceptsMyInterface(x: MyInterface) { /* ... */ }
acceptsMyInterface(o); // Works!
acceptsMyInterface(foo); // Works!


function acceptsObj(x: {a: number, ...}) { /* ... */ }
acceptsObj(o); // Works!
acceptsObj(foo); // Error!

Причина кроется в отвязке методов: Flow отслеживает this у методов класса, и возможность передать экземпляр класса в тип объекта стала бы обходным путём вокруг этого правила — извлечь obj.method через псевдоним типа объекта и вызвать его без получателя. С интерфейсами возникает та же проблема, поэтому значение с типом интерфейса также нельзя передать в тип объекта. Обычные литералы объектов не несут привязки this, которую можно потерять, поэтому их можно свободно передавать в типы объектов.

Таким образом, три вида значений асимметрично соотносятся с двумя структурными типами:

значение можно присвоить типу объекта можно присвоить интерфейсу
литерал объекта ✓ ✓
экземпляр класса ✓
значение с типом интерфейса ✓

Код ошибки зависит от точности типа объекта: для точного типа объекта (по умолчанию) это [incompatible-exact]; для неточного типа объекта ({a: number, ...}) это [class-object-subtyping] с предложением переписать тип объекта как интерфейс. В обоих случаях решение — заменить тип параметра на интерфейс: интерфейсы принимают значения всех трёх видов.

В отличие от объектов, интерфейсы не могут быть точными, поскольку у них всегда могут быть другие, неизвестные свойства.

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

Можно использовать implements, чтобы сообщить Flow, что класс должен соответствовать интерфейсу. Это не позволит внести несовместимые изменения при редактировании класса.

interface Serializable {
  serialize(): string;
}


class Foo implements Serializable {
  serialize(): string { return '[Foo]'; } // Works!
}


class Bar implements Serializable {
  serialize(): number { return 42; } // Error!
}

Также можно использовать implements с несколькими интерфейсами.

interface Bar {}
interface Baz {}


class Foo implements Bar, Baz {
  // ...
}

Синтаксис интерфейсов​

Интерфейсы создаются с помощью ключевого слова interface, за которым следует имя и блок с телом определения типа.

interface MyInterface {
  // ...
}

Синтаксис блока совпадает с синтаксисом типов объектов.

Анонимные интерфейсы​

Также можно объявить анонимный интерфейс непосредственно в коде:

class Foo {
  a: number;
}


function getNumber(o: interface {a: number}): number {
  return o.a;
}


getNumber(new Foo()); // Works!

Методы интерфейсов​

В интерфейсы можно добавлять методы, используя тот же синтаксис, что и для методов класса. Любые указанные вами параметры this также подчиняются тем же ограничениям, что и методы класса.

interface MyInterface {
  method(value: string): number;
}

Как и методы класса, методы интерфейса также должны оставаться привязанными к интерфейсу, в котором они определены.

Можно определять перегруженные методы, объявляя одно и то же имя метода несколько раз с разными сигнатурами типов:

interface MyInterface {
  method(value: string): string;
  method(value: boolean): boolean;
}


function func(a: MyInterface) {
  const x: string = a.method('hi'); // Works!
  const y: boolean = a.method(true); // Works!


  const z: boolean = a.method('hi'); // Error!
}

Свойства интерфейсов​

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

interface MyInterface {
  property: string;
}

Свойства интерфейсов также могут быть необязательными:

interface MyInterface {
  property?: string;
}

Интерфейсы как отображения​

Можно создавать индексируемые свойства так же, как и в объектах:

interface MyInterface {
  [key: string]: number;
}

Обобщённые типы интерфейсов​

Интерфейсы также могут иметь собственные обобщённые параметры:

interface MyInterface<A, B, C> {
  property: A;
  method(val: B): C;
}

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

interface MyInterface<A, B, C> {
  foo: A;
  bar: B;
  baz: C;
}


const val: MyInterface<number, boolean, string> = {
  foo: 1,
  bar: true,
  baz: 'three',
};

Примитивы не являются подтипами интерфейсов​

Flow не позволяет передавать примитивы (string, number, boolean) в интерфейс или тип объекта, даже если их упакованный прототип соответствует контракту. Риск заключается в неоднозначности структуры, которая остаётся незамеченной: обычная строка string структурно соответствует Iterable<string> (через String.prototype[Symbol.iterator]), но при её переборе получаются кодовые точки, а не элементы — почти всегда это логическая ошибка, а не ожидаемое поведение.

function logAll(items: Iterable<string>) {
  for (const x of items) console.log(x);
}


const msgs = "foo";
logAll(msgs); // Error: `string` is not a subtype of `Iterable<string>`

Если предполагался итерируемый объект с одним элементом, оберните значение в [s] в месте вызова; в противном случае вызову передавалась структура неверной формы.

Расширение интерфейсов​

Интерфейс может расширять другие интерфейсы или классы, наследуя их члены:

interface Animal {
  name: string;
}
interface Dog extends Animal {
  breed: string;
}


const d: Dog = {name: "Rex", breed: "Lab"};
const a: Animal = d; // Works!

В правой части extends должно быть указано имя интерфейса или класса — передача псевдонима типа объекта приводит к ошибке [incompatible-use] («не является наследуемым»). То же ограничение действует для конструкции implements в классе ([cannot-implement]). Типы с отображением или служебные типы, применённые к интерфейсам, создают типы объектов, поэтому они также не подходят ни для одной из этих конструкций. Решение — объявить именованный интерфейс (или добавить его члены непосредственно).

Вариантность свойств интерфейсов (только для чтения и только для записи)​

По умолчанию свойства интерфейсов инвариантны. Однако можно добавить модификаторы, чтобы сделать их ковариантными (только для чтения) или контравариантными (только для записи).

interface MyInterface {
  readonly covariant: number;      // read-only
  writeonly contravariant: number; // write-only
}

Ковариантные свойства интерфейсов (только для чтения)​

Свойство можно сделать ковариантным, добавив ключевое слово readonly перед его именем:

interface MyInterface {
  readonly readOnly: number | string;
}

Это позволяет передать вместо этого свойства более конкретный тип:

interface Invariant {
  property: number | string;
}
interface Covariant {
  readonly readOnly: number | string;
}


const x: {property: number} = {property: 42};
const y: {readOnly: number} = {readOnly: 42};


const value1: Invariant = x; // Error!
const value2: Covariant = y; // Works

Из-за особенностей ковариантности такие свойства также становятся доступными только для чтения при использовании. Это может быть полезно по сравнению с обычными свойствами.

interface Invariant {
  property: number | string;
}
interface Covariant {
  readonly readOnly: number | string;
}


function func1(value: Invariant) {
  value.property;        // Works!
  value.property = 3.14; // Works!
}


function func2(value: Covariant) {
  value.readOnly;        // Works!
  value.readOnly = 3.14; // Error!
}

Контравариантные свойства интерфейсов (только для записи)​

Свойство можно сделать контравариантным, добавив ключевое слово writeonly перед его именем.

interface InterfaceName {
  writeonly writeOnly: number;
}

Это позволяет передать вместо этого свойства менее конкретный тип.

interface Invariant {
  property: number;
}
interface Contravariant {
  writeonly writeOnly: number;
}


const numberOrString = Math.random() > 0.5 ? 42 : 'forty-two';


const value1: Invariant     = {property: numberOrString};  // Error!
const value2: Contravariant = {writeOnly: numberOrString}; // Works!

Из-за особенностей контравариантности такие свойства также становятся доступными только для записи при использовании. Это может быть полезно по сравнению с обычными свойствами.

interface Invariant {
  property: number;
}
interface Contravariant {
  writeonly writeOnly: number;
}


function func1(value: Invariant) {
  value.property;        // Works!
  value.property = 3.14; // Works!
}


function func2(value: Contravariant) {
  value.writeOnly;        // Error!
  value.writeOnly = 3.14; // Works!
}

См. также​

  • Классы — значения с номинальной типизацией, которые могут реализовывать интерфейсы
  • Объекты — типы объектов со структурной типизацией, которые не могут описывать экземпляры классов
  • Номинальная и структурная типизация — различия между типизацией по имени и по структуре
  • Вариантность — ковариантные свойства (только для чтения) и контравариантные свойства (только для записи)
  • Модернизация устаревшего синтаксиса Flow — переход от устаревших обозначений вариантности свойств + / - к ключевым словам readonly / writeonly

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

Spec-Zone.ru

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