Spec-Zone.ru › Flow

Вариантность типов

Вариантность — тема, которая довольно часто встречается в системах типов. Она используется, чтобы определить, как параметры типов ведут себя относительно подтипов.

Сначала создадим пару классов, которые наследуются друг от друга.

class Noun {}
class City extends Noun {}
class SanFrancisco extends City {}

В разделе о обобщённых типах мы увидели, что ключевые слова вариантности позволяют указать, когда параметр типа используется в выходной позиции, когда он используется во входной позиции и когда он используется в обеих позициях.

Здесь мы подробнее рассмотрим каждый из этих случаев.

Сравнение с TypeScript

Flow по умолчанию использует более строгую вариантность, чем TypeScript, во всех позициях, где они различаются: изменяемые свойства объектов, изменяемые массивы, параметры обобщённых типов и параметры методов классов. Ключевые слова вариантности в основном совпадают (readonly для свойств, in / out для параметров типов); writeonly есть только в Flow, а у <in out T> из TS нет аналога в Flow, поскольку Flow по умолчанию уже использует инвариантность. Разбивку по позициям см. в разделе Сравнение вариантности в Flow и TypeScript.

Ковариантность​

Рассмотрим, например, тип

type CovariantOf<X> = {
  readonly prop: X;
  getter(): X;
}

Здесь X используется исключительно в выходных позициях: он служит для чтения данных из объектов o типа CovariantOf<X> — либо при обращении к свойствам o.prop, либо при вызове o.getter().

Важно отметить, что через ссылку на объект o нельзя передать данные, поскольку prop — свойство только для чтения.

Если эти условия соблюдены, мы можем использовать ключевое слово out, чтобы аннотировать X в определении CovariantOf:

type CovariantOf<out X> = {
  readonly prop: X;
  getter(): X;
}

Эти условия существенно влияют на то, как мы можем использовать объект типа CovariantOf<T> с учётом подтипов. Напомним, что правила подтипов помогают ответить на вопрос: «Если некоторый контекст ожидает значения типа T, безопасно ли передать ему значения типа S?» Если это так, то S — подтип T.

Используя определение CovariantOf и зная, что City — подтип Noun, мы также можем заключить, что CovariantOf<City> — подтип CovariantOf<Noun>. Действительно:

  • безопасно прочитать свойство prop типа City, когда ожидается свойство типа Noun, и
  • безопасно вернуть значения типа City при вызове getter(), когда ожидаются значения типа Noun.

В сочетании эти два условия означают, что CovariantOf<City> всегда можно безопасно использовать там, где ожидается CovariantOf<Noun>.

Распространённый пример использования ковариантности — ReadonlyArray<T>. Как и в случае со свойством prop, через ссылку ReadonlyArray нельзя записывать данные в массив. Это позволяет применять более гибкие правила подтипов: Flow достаточно доказать, что S — подтип T, чтобы установить, что ReadonlyArray<S> также является подтипом ReadonlyArray<T>.

Тип this допускается только в ковариантных позициях​

К типу this применяется то же правило. Он допускается в ковариантных позициях — в типах возвращаемых значений методов и полях readonly, — но запрещён в параметрах методов и изменяемых полях; в таких случаях возникает [incompatible-variance]:

class Builder {
  add(x: number): this { return this; } // OK: return type is covariant
  readonly origin: this | null = null; // OK: readonly field is covariant
  takesSelf(other: this): void {} // ERROR: input position
  parent: this | null = null; // ERROR: invariant field
}

Это следует из той же модели, согласно которой изменяемые свойства объектов инвариантны: если бы parent: this | null разрешалось использовать в базовом Builder, экземпляр подкласса можно было бы передать как Builder, записать обычный Builder в его parent, а последующий sb.parent.subclassMethod() прошёл бы проверку типов, но завершился бы ошибкой во время выполнения — поэтому Flow полностью запрещает такое поле.

Чтобы исправить эту ошибку, укажите класс явно во входной позиции или позиции поля (other: Builder, parent: Builder | null) и смиритесь с потерей типа подкласса в этой позиции — либо пометьте поле как readonly, чтобы позиция стала ковариантной.

Инвариантность​

Посмотрим, что произойдёт, если ослабить ограничения на использование X и, например, сделать prop свойством для чтения и записи. Получим такое определение типа:

type NonCovariantOf<X> = {
  prop: X;
  getter(): X;
};

Объявим также переменную nonCovariantCity типа NonCovariantOf<City>. Теперь считать nonCovariantCity объектом типа NonCovariantOf<Noun> небезопасно. Если бы это было допустимо, мы могли бы записать Noun в prop, нарушив исходный тип. Flow обнаруживает это:

class Noun {}
class City extends Noun {}


type NonCovariantOf<X> = {
  prop: X;
  getter(): X;
};


declare const nonCovariantCity: NonCovariantOf<City>;
const nonCovariantNoun: NonCovariantOf<Noun> = nonCovariantCity; // Error!

Отличие NonCovariantOf от определения CovariantOf в том, что параметр типа X используется как во входных, так и в выходных позициях, поскольку он служит и для чтения, и для записи свойства prop. Такой параметр типа называется инвариантным. Это вариантность по умолчанию, поэтому ключевое слово перед ним не требуется:

type InvariantOf<X> = {
  prop: X;
  getter(): X;
  setter(X): void;
};

Пусть переменная invariantCity имеет тип InvariantOf<City>. Использовать invariantCity небезопасно в контексте, где:

  • требуется InvariantOf<Noun>, поскольку мы не должны иметь возможности записать Noun в свойство prop.
  • требуется InvariantOf<SanFrancisco>, поскольку при чтении prop может быть возвращён City, который может не быть SanFrancisco.

Иными словами, InvariantOf<City> не является ни подтипом InvariantOf<Noun>, ни подтипом InvariantOf<SanFrancisco>.

Контравариантность​

Если параметр типа используется только во входных позициях, мы говорим, что он используется контравариантно. Это означает, что он встречается только в позициях, через которые данные записываются в структуру. Для такого параметра типа используется ключевое слово in, а ключевое слово writeonly помечает контравариантное свойство:

type ContravariantOf<in X> = {
  writeonly prop: X;
  setter(X): void;
};

К типичным контравариантным позициям относятся свойства только для записи и функции-«сеттеры».

Объект типа ContravariantOf<City> можно использовать везде, где ожидается объект типа ContravariantOf<SanFrancisco>, но не там, где требуется ContravariantOf<Noun>. Иными словами, ContravariantOf<City> — подтип ContravariantOf<SanFrancisco>, но не ContravariantOf<Noun>. Это объясняется тем, что безопасно записать SanFrancisco в свойство, куда можно записать любое City, но небезопасно записывать туда произвольное Noun.

Контравариантность параметров функций​

Параметры функций всегда находятся во входной (контравариантной) позиции. Это означает, что функцию, принимающую более конкретный тип, нельзя использовать вместо функции, принимающей более общий тип. Это часто удивляет разработчиков, когда они передают функции обратного вызова с точными типами объектов:

type Exact = {foo: string};
type Inexact = {foo: string, ...};


declare function acceptsExact(item: Exact): void;
declare function takesCallback(cb: (item: Inexact) => void): void;


takesCallback(acceptsExact); // Error!

Эта ошибка возникает потому, что takesCallback может вызвать cb с объектом, у которого есть дополнительные свойства (поскольку Inexact допускает их). Функция обратного вызова acceptsExact принимает только объекты, имеющие в точности {foo: string}, поэтому передача ей объекта с неточным типом была бы небезопасной. Хотя передать точный объект непосредственно функции, ожидающей неточный объект, можно (точный тип является подтипом совместимого неточного типа), типы функций меняются местами из-за контравариантности.

Входные и выходные позиции​

В сообщениях об ошибках вариантности Flow использует термины «входные позиции» и «выходные позиции». Они напрямую соответствуют описанным выше понятиям вариантности:

  • Выходная позиция — место, откуда значение читается из типа: типы возвращаемых значений, свойства только для чтения, результаты геттеров. Параметр типа, помеченный out (ковариантный), может находиться только в выходных позициях.
  • Входная позиция — место, куда значение записывается в тип: параметры функций, свойства только для записи, аргументы сеттеров. Параметр типа, помеченный in (контравариантный), может находиться только во входных позициях.
  • Параметр типа без ключевого слова (инвариантный) может находиться как во входных, так и в выходных позициях.

Увидев ошибку, например «Нельзя использовать T во входной позиции, поскольку T должен встречаться только в выходных позициях», вы узнаете, что параметр типа помечен как ковариантный (out T), но используется в позиции, куда записывается значение, например в параметре функции. Flow сообщает об этом как о [incompatible-variance]:

type Box<out T> = {
  get(): T;
  set(val: T): void; // Error [incompatible-variance]: T is in an input position but is expected only in output positions
};

Исправление зависит от того, чего вы хотите добиться: если тип действительно должен и читать, и записывать T, удалите ключевое слово out, чтобы сделать T инвариантным. Если тип должен только возвращать значения типа T (но никогда не принимать их), удалите сеттер.

См. также​

  • Подтипы — лежащие в основе отношения подтипов, на которых строится вариантность
  • Обобщённые типы — ключевые слова вариантности для параметров обобщённых типов
  • Массивы — ReadonlyArray (ковариантный) и Array (инвариантный)
  • Интерфейсы — ковариантные и контравариантные свойства интерфейсов
  • Объекты — свойства объектов только для чтения и только для записи
  • Модернизация устаревшего синтаксиса Flow — переход с обозначений вариантности + / - на ключевые слова readonly / writeonly и in / out

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

Spec-Zone.ru

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