Spec-Zone.ru › Flow

Объекты

Типы объектов описывают структуру объектов JavaScript.

const obj: {foo: number, bar: boolean} = {foo: 1, bar: true};

Flow поддерживает несколько вариантов:

  • Точные типы объектов (по умолчанию): ровно заданный набор свойств, например {a: number}.
  • Неточные типы объектов: как минимум заданный набор свойств, например {a: number, ...}.
  • Индексируемые типы объектов: используются как карты, например {[string]: boolean}.
Сравнение с TypeScript

Типы объектов TypeScript выглядят так же, но по умолчанию являются открытыми: {x: number} допускает дополнительные свойства в TypeScript, тогда как точный тип по умолчанию в Flow {x: number} их отвергает. Проверка избыточных свойств в TypeScript выполняется только при непосредственном присваивании литерала. Эквивалент открытой структуры TypeScript в Flow записывается как неточная форма {x: number, ...}. Есть и два связанных различия: подтипизация классов, интерфейсов и объектов в Flow асимметрична, поскольку классы типизируются номинально, а при комбинировании типов объектов Flow использует операцию spread на уровне типов {...A, b: T} вместо пересечения типов в TypeScript A & {b: T}.

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

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

Необязательные свойства типа объекта​

В JavaScript обращение к несуществующему свойству возвращает undefined. Это распространённый источник ошибок в программах на JavaScript, поэтому Flow превращает такие случаи в ошибки типов.

const obj = {foo: "bar"};
obj.bar; // Error!

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

const obj: {foo?: boolean} = {};


obj.foo = true;    // Works!
obj.foo = 'hello'; // Error!

Помимо указанного типа значения, такие необязательные свойства могут иметь значение void или отсутствовать вовсе. Однако они не могут быть null.

function acceptsObject(value: {foo?: string}) { /* ... */ }


acceptsObject({foo: "bar"});     // Works!
acceptsObject({foo: undefined}); // Works!
acceptsObject({});               // Works!


acceptsObject({foo: null});      // Error!

Чтобы сделать необязательными все свойства типа объекта, можно использовать вспомогательный тип Partial:

type Obj = {
  foo: string,
};


type PartialObj = Partial<Obj>; // Same as `{foo?: string}`

Чтобы сделать обязательными все свойства типа объекта, можно использовать вспомогательный тип Required:

type PartialObj = {
  foo?: string,
};


type Obj = Required<PartialObj>; // Same as `{foo: string}`

Свойства объекта только для чтения​

К свойствам объектов можно добавлять аннотации вариантности.

Чтобы пометить свойство как доступное только для чтения, можно использовать ключевое слово readonly.

type Obj = {
  readonly foo: string,
};


function func(o: Obj) {
  const x: string = o.foo; // Works!
  o.foo = 'hi'; // Error!
}

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

type Obj = {
  foo: string,
};


type ReadOnlyObj = Readonly<Obj>; // Same as `{readonly foo: string}`

Также свойства можно пометить как доступные только для записи с помощью ключевого слова writeonly.

type Obj = {
  writeonly foo: string,
};


function func(o: Obj) {
  const x: string = o.foo; // Error!
  o.foo = 'hi'; // Works!
}
Устаревший синтаксис с сигилами

Ключевые слова readonly и writeonly включены по умолчанию начиная с Flow 0.315. В старом коде могут использоваться устаревшие сигилы + и - (например, {+foo: string} вместо {readonly foo: string}, {-foo: string} вместо {writeonly foo: string}); они по-прежнему распознаются, но считаются устаревшими. Сведения о переходе см. в разделе Обновление устаревшего синтаксиса.

Методы объектов​

Синтаксис методов в объектах имеет то же поведение во время выполнения, что и свойство-функция. Эти два объекта эквивалентны во время выполнения:

const a = {
  foo: function () { return 3; }
};
const b = {
  foo() { return 3; }
}

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

const b = {
  foo() { return 3; }
}
b.foo = () => { return 2; } // Error!

Кроме того, в телах методов объектов нельзя использовать this, чтобы гарантировать простое поведение их параметров this. Вместо this лучше обращаться к объекту по имени.

const a = {
  x: 3,
  foo() { return this.x; } // Error!
}
const b = {
  x: 3,
  foo(): number { return b.x; } // Works!
}

Поскольку сокращённый синтаксис метода доступен только для чтения, а поле с типом функции изменяемо, у них различается вариантность свойства. Сокращённая запись метода задействует только контравариантность входных типов функции, поэтому функцию с более широким входным типом можно присвоить слоту метода с более узким входным типом. Поле с типом функции является инвариантным, поэтому такое же присваивание будет отклонено:

type ByMethod = {compare(a: string, b: string): number};
type ByField = {compare: (a: string, b: string) => number};
declare const wider: {compare: (a: string | number, b: string | number) => number};


const m: ByMethod = wider; // Works — method shorthand is read-only
const f: ByField = wider;  // Error — function-typed field is mutable and therefore invariant


type ByReadonlyField = {readonly compare: (a: string, b: string) => number};
const rof: ByReadonlyField = wider; // Works — readonly field is covariant, like method shorthand

Пометка поля как readonly восстанавливает ковариантность свойства, поэтому третье присваивание проходит, хотя присваивание изменяемому полю было отклонено. О различиях с бивариантностью TypeScript и подробностях механизма см. раздел Контравариантность параметров методов в Flow.

Вывод типов объектов​

При создании значения объекта его тип определяется в момент создания. Нельзя добавлять новые свойства или изменять тип существующих свойств.

const obj = {
  foo: 1,
  bar: true,
};


const n: number  = obj.foo; // Works!
const b: boolean = obj.bar; // Works!


obj.UNKNOWN; // Error - prop `UNKNOWN` is not in the object value
obj.foo = true; // Error - `foo` is of type `number`

Если указать аннотацию типа, в качестве необязательных свойств можно добавить отсутствующие в значении объекта свойства:

const obj: {
  foo?: number,
  bar: boolean,
} = {
  // `foo` is not set here
  bar: true,
};


const n: number | void = obj.foo; // Works!
const b: boolean = obj.bar; // Works!


if (b) {
  obj.foo = 3; // Works!
}

Также можно задать для определённого свойства более широкий тип:

const obj: {
  foo: number | string,
} = {
  foo: 1,
};


const foo: number | string = obj.foo; // Works!
obj.foo = "hi"; // Works!

Пустой объект можно интерпретировать как словарь, если указать соответствующую аннотацию типа:

const dict: {[string]: number} = {}; // Works!

Если литерал объекта рекурсивно ссылается сам на себя (за исключением простых случаев), может понадобиться добавить аннотации типов:

const Utils = { // Error
  foo() {
    return Utils.bar();
  },
  bar() {
    return 1;
  }
};


const FixedUtils = { // Works!
  foo(): number {
    return FixedUtils.bar();
  },
  bar(): number {
    return 1;
  }
};

Точные и неточные типы объектов​

По умолчанию типы объектов являются точными.

Неточные объекты (обозначаемые с помощью ...) допускают передачу дополнительных свойств:

function method(obj: {foo: string, ...}) { /* ... */ }


method({foo: "test", bar: 42}); // Works!

Примечание: Это связано с «подтипизацией по ширине».

Точные типы объектов этого не допускают:

function method(obj: {foo: string}) { /* ... */ }


method({foo: "test", bar: 42}); // Error!

Пересечения точных типов объектов обычно являются невозможными типами: значение должно было бы в точности соответствовать A и одновременно в точности соответствовать B, что невозможно, как только A и B хоть чем-то различаются. Для объединения точных типов объектов используется операция spread для типов объектов, которая напрямую повторяет семантику spread для значений во время выполнения: учитываются только собственные свойства, более поздние ключи перезаписывают более ранние, а точность сохраняется:

type FooT = {foo: string};
type BarT = {bar: number};


type FooBarT = {...FooT, ...BarT};
const fooBar: FooBarT = {foo: '123', bar: 12}; // Works!


type FooBarFailT = FooT & BarT;
const fooBarFail: FooBarFailT = {foo: '123', bar: 12}; // Error!

Операция spread для типов объектов​

Как можно применять spread к значениям объектов, так же можно применять его и к типам объектов:

type ObjA = {
  a: number,
  b: string,
};


const x: ObjA = {a: 1, b: "hi"};


type ObjB = {
  ...ObjA,
  c: boolean,
};


const y: ObjB = {a: 1, b: 'hi', c: true}; // Works!
const z: ObjB = {...x, c: true}; // Works!

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

type Inexact = {
  a: number,
  b: string,
  ...
};


type ObjB = { // Error!
  c: boolean,
  ...Inexact, // Error
};


const x: ObjB = {a:1, b: 'hi', c: true};

Та же проблема возникает с объектами, имеющими индексаторы, поскольку у них тоже есть неизвестные ключи:

type Dict = {
  [string]: number,
};


type ObjB = { // Error!
  c: boolean,
  ...Dict,
};


const x: ObjB = {a: 1, b: 2, c: true};

Во время выполнения spread значения объекта включает только «собственные» свойства, то есть свойства непосредственно самого объекта, а не цепочки прототипов. Операция spread для типов объектов работает так же. Поэтому к интерфейсам нельзя применять spread: они не отслеживают, является ли свойство «собственным»:

interface Iface {
  a: number;
  b: string;
}


type ObjB = { // Error!
  c: boolean,
  ...Iface,
};


const x: ObjB = {a: 1, b: 'hi', c: true};

Объекты как карты​

В JavaScript есть класс Map, но объекты по-прежнему очень часто используют и как карты. В этом случае свойства объекта, скорее всего, будут добавляться и извлекаться на протяжении всего его жизненного цикла. Кроме того, ключи свойств могут быть заранее неизвестны, поэтому написать аннотацию типа будет невозможно.

Для таких объектов Flow предоставляет особый вид свойств — «свойство-индексатор». Свойство-индексатор позволяет читать и записывать значения по любому ключу, соответствующему типу ключа индексатора.

const o: {[string]: number} = {};
o["foo"] = 0;
o["bar"] = 1;
const foo: number = o["foo"];

Для целей документации индексатору можно задать имя:

const obj: {[user_id: number]: string} = {};
obj[1] = "Julia";
obj[2] = "Camille";
obj[3] = "Justin";
obj[4] = "Mark";

Если тип объекта содержит свойство-индексатор, предполагается, что обращения к свойствам имеют аннотированный тип, даже если во время выполнения в соответствующей ячейке нет значения. Как и в случае с массивами, программист отвечает за безопасность такого обращения.

const obj: {[number]: string} = {};
obj[42].length; // No type error, but will throw at runtime

Индексируемые свойства можно сочетать с именованными свойствами:

const obj: {
  size: number,
  [id: number]: string
} = {
  size: 0
};


function add(id: number, name: string) {
  obj[id] = name;
  obj.size++;
}

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

type ReadOnly = {readonly [string]: number};
type WriteOnly = {writeonly [string]: number};

Ключи, значения и индексированный доступ​

Извлечь ключи типа объекта можно с помощью вспомогательного типа keyof:

type Obj = {
  foo: string,
  bar: number,
};


type T = keyof Obj;


function acceptsKeys(k: T) { /* ... */ }


acceptsKeys('foo'); // Works!
acceptsKeys('bar'); // Works!
acceptsKeys('hi'); // Error!

Извлечь значения типа объекта можно с помощью вспомогательного типа Values:

type Obj = {
  foo: string,
  bar: number,
};


type T = Values<Obj>;


function acceptsValues(v: T) { /* ... */ }


acceptsValues(2); // Works!
acceptsValues('hi'); // Works!
acceptsValues(true); // Error!

Получить тип конкретного свойства типа объекта можно с помощью типов индексированного доступа:

type Obj = {
  foo: string,
  bar: number,
};


type T = Obj['foo'];


function acceptsStr(x: T) { /* ... */ }


acceptsStr('hi'); // Works!
acceptsStr(1); // Error!

Произвольные объекты​

Если нужно безопасно принимать произвольный объект, можно воспользоваться несколькими способами.

Пустой неточный объект {...} принимает любой объект:

function func(obj: {...}) {
  // ...
}


func({}); // Works!
func({a: 1, b: "foo"}); // Works!

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

function func<T extends {...}>(obj: T) {
  // ...
}


func({}); // Works!
func({a: 1, b: "foo"}); // Works!

Однако у {...} нельзя получать доступ к свойствам.

Также можно использовать словарь со значениями типа unknown, который позволяет обращаться к любому свойству (результирующий тип будет unknown):

function func(obj: {readonly [string]: unknown}) {
  const x: unknown = obj['bar'];
}


func({}); // Works!
func({a: 1, b: "foo"}); // Works!

Тип Object — это всего лишь псевдоним для any, и его использование небезопасно. Запретить его в коде можно с помощью правила линтера unclear-type.

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

Доступ к свойству по вычисляемому ключу (invalid-computed-prop)​

Flow выдаёт ошибку invalid-computed-prop, если вычисляемый ключ используется для доступа к объекту без типа индексатора. Flow должен знать точный набор ключей на уровне типов, поэтому произвольный доступ по вычисляемому ключу к обычным типам объектов запрещён:

const obj = {a: 1, b: 2};


const key: string = "a";
obj[key]; // Error! Can't use string to index into an object with known keys.

Чтобы исправить это, есть несколько вариантов:

Используйте keyof typeof, чтобы сузить тип ключа, если известно, что ключ допустим:

const obj = {a: 1, b: 2};


function getVal(key: keyof typeof obj): number {
  return obj[key]; // Works!
}

Используйте тип индексатора, если объект предназначен для использования в качестве словаря:

const obj: {[string]: number} = {a: 1, b: 2};


const key: string = "a";
obj[key]; // Works!

Экспоненциальный рост при операции spread для типов​

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

declare const cond1: boolean;
declare const cond2: boolean;
declare const cond3: boolean;


// Each conditional spread doubles the number of possible types
const obj = {
  ...cond1 ? {a: 1} : {},
  ...cond2 ? {b: 2} : {}, // Error: too many combinations to reason about
  ...cond3 ? {c: 3} : {},
  // Flow must consider 2^3 = 8 possible object types
};

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

type Flags = {
  a?: number,
  b?: number,
  c?: number,
};


declare const cond1: boolean;
declare const cond2: boolean;


const flags1: Flags = cond1 ? {a: 1} : {};
const flags2: Flags = cond2 ? {b: 2} : {};
const obj: Flags = {...flags1, ...flags2};

См. также​

  • Интерфейсы — структурные типы, описывающие как экземпляры классов, так и объекты
  • Классы — значения с номинальной типизацией; используйте интерфейсы для взаимодействия классов с типами объектов
  • Кортежи — коллекции фиксированной длины с типами для каждого элемента
  • Типы индексированного доступа — получение типов конкретных свойств из типов объектов
  • Вспомогательные типы — Partial, Required, Readonly, keyof и другие вспомогательные средства для работы с объектами
  • Вариантность — ковариантные свойства (только для чтения) и контравариантные свойства (только для записи)
  • Подтипизация по глубине — взаимодействие подтипизации по ширине и глубине с типами объектов
  • Отображённые типы — систематическое преобразование типов свойств объекта
  • Обновление устаревшего синтаксиса Flow — переход от устаревшего точного синтаксиса {| ... |}, сигилов свойств + / - и $Exact<T> к современным формам

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

Spec-Zone.ru

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