Совместная работа записей ImmutableJS и потока

Примечание. Этот блог был написан 6 мая 2017 г. Из-за того, что информация зависит от времени, часть содержимого может быть устаревшей.

Если вы используете ImmutableJS и Flow, возможно, вы столкнулись с трудностями, заставляя записи работать. Последний крупный выпуск ImmutableJS, v3.8.1, очень мало поддерживает типы записей Flow. Версия 4.0.0 содержит гораздо лучшую поддержку, но, поскольку это все еще RC-2, вы можете не захотеть использовать последнюю версию для производственного кода. Кроме того, даже код RC-2 все еще имеет некоторые проблемы. Поскольку записи очень полезны, вот способ заставить их работать в вашем проекте.

Короче говоря, временным решением является создание настраиваемого типа неизменяемого потока на основе определений потока RC-2 и патча из одного из запросов на вытягивание.

Создание пользовательского неизменяемого типа потока

Сначала скопируйте файл immutable.js.flow из версии RC-2 в папку с типизированным потоком, предполагая, что вы используете инструмент типизированный поток для переноса типов потока в папку с версиями. Назовите файл как-то вроде immutable_v4.x.x.js, чтобы следовать соглашению о потоковом типе. Зарегистрируйте этот файл в системе контроля версий.

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

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

Сначала оберните весь код в объявление модуля:

declare module 'immutable' { }

Затем удалите все экспорты внизу файла. Типы, объявленные в определении модуля, не нужно явно экспортировать, и они вызывают ошибки, если выполняются в определении с потоковым типом. Я не совсем уверен, почему файлы Flow в пакетах node_module обрабатываются иначе, чем определения Flow в библиотеке потокового типа. Если кто-нибудь знает, дайте мне знать.

Затем закомментируйте следующее в строке 232, поскольку оно вызывает ошибку потока и в настоящее время является просто заполнителем:

// Collection.Keyed = KeyedCollection

Последнее необходимое изменение - это применение патча к определению записи. Замените все определения типов записей (строки 1056–1116) следующим фрагментом:

declare function isRecord(maybeRecord: any): boolean %checks(maybeRecord instanceof RecordClass);
declare class Record {
  static <Values>(spec: Values, name?: string): Class<RecordClass<Values>>;
  constructor<Values>(spec: Values, name?: string): Class<RecordClass<Values>>;

  static isRecord: typeof isRecord;

  static getDescriptiveName(record: RecordClass<*>): string;
}

declare class RecordClass<T: Object> {
  static (values?: $Shape<T> | Iterable<[string, any]>): RecordClass<T> & T;
  constructor(values?: $Shape<T> | Iterable<[string, any]>): RecordClass<T> & T;

  size: number;

  has(key: string): boolean;
  get<K: $Keys<T>>(key: K): /*T[K]*/any;

  equals(other: any): boolean;
  hashCode(): number;

  set<K: $Keys<T>>(key: K, value: /*T[K]*/any): this;
  update<K: $Keys<T>>(key: K, updater: (value: /*T[K]*/any) => /*T[K]*/any): this;
  merge(...collections: Array<$Shape<T> | Iterable<[string, any]>>): this;
  mergeDeep(...collections: Array<$Shape<T> | Iterable<[string, any]>>): this;

  mergeWith(
    merger: (oldVal: any, newVal: any, key: $Keys<T>) => any,
    ...collections: Array<$Shape<T> | Iterable<[string, any]>>
  ): this;
  mergeDeepWith(
    merger: (oldVal: any, newVal: any, key: any) => any,
    ...collections: Array<$Shape<T> | Iterable<[string, any]>>
  ): this;

  delete<K: $Keys<T>>(key: K): this;
  remove<K: $Keys<T>>(key: K): this;
  clear(): this;

  setIn(keyPath: Iterable<any>, value: any): this;
  updateIn(keyPath: Iterable<any>, updater: (value: any) => any): this;
  mergeIn(keyPath: Iterable<any>, ...collections: Array<any>): this;
  mergeDeepIn(keyPath: Iterable<any>, ...collections: Array<any>): this;
  deleteIn(keyPath: Iterable<any>): this;
  removeIn(keyPath: Iterable<any>): this;

  toSeq(): KeyedSeq<$Keys<T>, any>;

  toJS(): { [key: $Keys<T>]: mixed };
  toJSON(): T;
  toObject(): T;

  withMutations(mutator: (mutable: this) => mixed): this;
  asMutable(): this;
  asImmutable(): this;

  @@iterator(): Iterator<[$Keys<T>, any]>;
}

По сути, патч избавляется от промежуточного представления интерфейса записи, в результате чего остаются «класс» записи и «класс» записи. Семантика класса для Flow и то, как на самом деле работает javascript, не совсем совпадают, но это имеет смысл, когда вы начинаете его использовать.

Последнее необходимое изменение - это изменение .flowconfig для игнорирования файла Flow, поставляемого с ImmutableJS. Добавьте в свой конфиг следующую строку:

.*/node_modules/immutable/*

Использование новых записей потока

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

/* @flow */
// file: ExampleComponent.jsx
import React, { Component } from 'react';
import { List, Record } from 'immutable';
const exampleRecord = Record(({
  displayValue: '',
  id: '',
}: {
  displayValue: string,
  id: string,
}));
// In this example, exampleRecordInstance is only used for type
// annotation
const exampleRecordInstance = exampleRecord();
type Props = {|
  listOfRecords: List<typeof exampleRecordInstance>,
|};
class ExampleComponent extends Component<void, Props, void> {
// {...}
}
export { exampleRecord };
export default ExampleComponent;

Главное помнить, что код обычно экспортирует «класс» записи, чтобы другие модули могли использовать его для создания записи. Но в данном случае для аннотаций используется тип typeof exampleRecordInstance, что создает линию шаблона, но это самое чистое решение, которое я когда-либо видел.

Примечание. К сожалению, здесь есть скрытая ошибка. Flow выдаст ошибку, если вы воспользуетесь вызовом «нового» конструктора перед созданием экземпляра записи. Сообщение об ошибке: «Тип вызова конструктора несовместим с каким-либо элементом пересечения типа пересечения». Если в этом нет большого смысла, вы не одиноки. Тип пересечения позволяет динамически определять свойства класса, но у Flow есть ограничения на использование типов пересечений (я думаю, поправьте меня, если я ошибаюсь). Поскольку использование слова «новый» здесь не работает, и поскольку большинство линтеров жалуются на переменные, начинающиеся с заглавных букв, мы используем обычные переменные в верблюжьем регистре, чтобы удовлетворить Flow и линтер.

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

/* @flow */
import React, { Component } from 'react';
import { List } from 'immutable';
import ExampleComponent, {
  exampleRecord,
} './ExampleComponent.jsx';
const UseExampleComponent = () => {
  const aNewInstance = exampleRecord({
    id: 'jim',
    displayValue: 'Jim',
  });  
  const aList = List();

  return (
    <ExampleComponent listOfRecords={aList.push(aNewInstance)}/>
  );
}
export default UseExampleComponent;

Если вам нужно аннотировать aList - не специально в этом случае, но если у вас есть переменные состояния одного и того же типа, например, - вы можете использовать экзистенциальный тип для типа экземпляра записи, потому что Flow может определить, что он должен соответствовать определенному в Компонент ExampleComponent. Эта строка будет выглядеть так:

const aList: List<*> = List();

Или, если вы хотите быть явным, вы можете сделать что-то вроде этого:

const aList: List<typeof aNewInstance> = List();

Что хорошо в аннотировании записей, так это то, что теперь будет проверяться множество вещей, таких как доступ к свойствам с использованием точечной нотации и использование методов, которые принимают ключи в виде строк - что наиболее важно, сами строки будут проверены с разрешенными свойствами в записи. Итак, используя приведенные выше примеры, aList.get(0).ids и aList.get(0).get('ids') будут генерировать ошибки Flow - при условии, что в списке есть действительная запись.

Кроме того, использование Flow уменьшает путаницу при использовании неизменяемых структур данных по сравнению с собственными, поскольку Flow знает контекст.

Как только ImmutableJS v4.0.0 будет официально выпущен, и PR будет объединен с исправлением ошибок, первая часть этой статьи будет устаревшей, но она будет работать до тех пор, пока вы не будете готовы к обновлению.