Объекты
Типы объектов описывают структуру объектов JavaScript.
const obj: {foo: number, bar: boolean} = {foo: 1, bar: true}; Flow поддерживает несколько вариантов:
-
Точные типы объектов (по умолчанию): ровно заданный набор свойств, например
{a: number}. -
Неточные типы объектов: как минимум заданный набор свойств, например
{a: number, ...}. -
Индексируемые типы объектов: используются как карты, например
{[string]: boolean}.
Типы объектов 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