Интерфейсы
Интерфейсы объявляют структуру, которой должен соответствовать класс или объект, не требуя определённой идентичности класса. Поскольку классы в 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 классы типизируются структурно, поэтому интерфейсы и типы объектов в значительной степени взаимозаменяемы. 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