/** @ignore we should disable this rules, but let's activate it to enable eslint first */
/**
 * Immutable data encourages pure functions (data-in, data-out) and lends itself
 * to much simpler application development and enabling techniques from
 * functional programming such as lazy evaluation.
 *
 * While designed to bring these powerful functional concepts to JavaScript, it
 * presents an Object-Oriented API familiar to Javascript engineers and closely
 * mirroring that of Array, Map, and Set. It is easy and efficient to convert to
 * and from plain Javascript types.
 *
 * ## How to read these docs
 *
 * In order to better explain what kinds of values the Immutable.js API expects
 * and produces, this documentation is presented in a statically typed dialect of
 * JavaScript (like [Flow][] or [TypeScript][]). You *don't need* to use these
 * type checking tools in order to use Immutable.js, however becoming familiar
 * with their syntax will help you get a deeper understanding of this API.
 *
 * **A few examples and how to read them.**
 *
 * All methods describe the kinds of data they accept and the kinds of data
 * they return. For example a function which accepts two numbers and returns
 * a number would look like this:
 *
 * ```js
 * sum(first: number, second: number): number
 * ```
 *
 * Sometimes, methods can accept different kinds of data or return different
 * kinds of data, and this is described with a *type variable*, which is
 * typically in all-caps. For example, a function which always returns the same
 * kind of data it was provided would look like this:
 *
 * ```js
 * identity<T>(value: T): T
 * ```
 *
 * Type variables are defined with classes and referred to in methods. For
 * example, a class that holds onto a value for you might look like this:
 *
 * ```js
 * class Box<T> {
 *   constructor(value: T)
 *   getValue(): T
 * }
 * ```
 *
 * In order to manipulate Immutable data, methods that we're used to affecting
 * a Collection instead return a new Collection of the same type. The type
 * `this` refers to the same kind of class. For example, a List which returns
 * new Lists when you `push` a value onto it might look like:
 *
 * ```js
 * class List<T> {
 *   push(value: T): this
 * }
 * ```
 *
 * Many methods in Immutable.js accept values which implement the JavaScript
 * [Iterable][] protocol, and might appear like `Iterable<string>` for something
 * which represents sequence of strings. Typically in JavaScript we use plain
 * Arrays (`[]`) when an Iterable is expected, but also all of the Immutable.js
 * collections are iterable themselves!
 *
 * For example, to get a value deep within a structure of data, we might use
 * `getIn` which expects an `Iterable` path:
 *
 * ```
 * getIn(path: Iterable<string | number>): unknown
 * ```
 *
 * To use this method, we could pass an array: `data.getIn([ "key", 2 ])`.
 *
 *
 * Note: All examples are presented in the modern [ES2015][] version of
 * JavaScript. Use tools like Babel to support older browsers.
 *
 * For example:
 *
 * ```js
 * // ES2015
 * const mappedFoo = foo.map(x => x * x);
 * // ES5
 * var mappedFoo = foo.map(function (x) { return x * x; });
 * ```
 *
 * [ES2015]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/New_in_JavaScript/ECMAScript_6_support_in_Mozilla
 * [TypeScript]: https://www.typescriptlang.org/
 * [Flow]: https://flowtype.org/
 * [Iterable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
 */

declare namespace Immutable {
  /** @ignore */
  type OnlyObject<T> = Extract<T, object>;

  /** @ignore */
  type ContainObject<T> =
    OnlyObject<T> extends object
      ? OnlyObject<T> extends never
        ? false
        : true
      : false;

  /**
   * @ignore
   *
   * Used to convert deeply all immutable types to a plain TS type.
   * Using `unknown` on object instead of recursive call as we have a circular reference issue
   */
  export type DeepCopy<T> =
    T extends Record<infer R>
      ? // convert Record to DeepCopy plain JS object
        {
          [key in keyof R]: ContainObject<R[key]> extends true
            ? unknown
            : R[key];
        }
      : T extends MapOf<infer R>
        ? // convert MapOf to DeepCopy plain JS object
          {
            [key in keyof R]: ContainObject<R[key]> extends true
              ? unknown
              : R[key];
          }
        : T extends Collection.Keyed<infer KeyedKey, infer V>
          ? // convert KeyedCollection to DeepCopy plain JS object
            {
              [key in KeyedKey extends PropertyKey
                ? KeyedKey
                : string]: V extends object ? unknown : V;
            }
          : // convert IndexedCollection or Immutable.Set to DeepCopy plain JS array
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            T extends Collection<infer _, infer V>
            ? Array<DeepCopy<V>>
            : T extends string | number // Iterable scalar types : should be kept as is
              ? T
              : T extends Iterable<infer V> // Iterable are converted to plain JS array
                ? Array<DeepCopy<V>>
                : T extends object // plain JS object are converted deeply
                  ? {
                      [ObjectKey in keyof T]: ContainObject<
                        T[ObjectKey]
                      > extends true
                        ? unknown
                        : T[ObjectKey];
                    }
                  : // other case : should be kept as is
                    T;

  /**
   * Describes which item in a pair should be placed first when sorting
   *
   * @ignore
   */
  export enum PairSorting {
    LeftThenRight = -1,
    RightThenLeft = +1,
  }

  /**
   * Function comparing two items of the same type. It can return:
   *
   * * a PairSorting value, to indicate whether the left-hand item or the right-hand item should be placed before the other
   *
   * * the traditional numeric return value - especially -1, 0, or 1
   *
   * @ignore
   */
  export type Comparator<T> = (left: T, right: T) => PairSorting | number;

  /**
   * @ignore
   *
   * KeyPath allowed for `xxxIn` methods
   */
  export type KeyPath<K> = OrderedCollection<K> | ArrayLike<K>;

  /**
   * Lists are ordered indexed dense collections, much like a JavaScript
   * Array.
   *
   * Lists are immutable and fully persistent with O(log32 N) gets and sets,
   * and O(1) push and pop.
   *
   * Lists implement Deque, with efficient addition and removal from both the
   * end (`push`, `pop`) and beginning (`unshift`, `shift`).
   *
   * Unlike a JavaScript Array, there is no distinction between an
   * "unset" index and an index set to `undefined`. `List#forEach` visits all
   * indices from 0 to size, regardless of whether they were explicitly defined.
   */
  namespace List {
    /**
     * True if the provided value is a List
     */
    function isList(maybeList: unknown): maybeList is List<unknown>;

    /**
     * Creates a new List containing `values`.
     *
     * Note: Values are not altered or converted in any way.
     */
    function of<T>(...values: Array<T>): List<T>;
  }

  /**
   * Create a new immutable List containing the values of the provided
   * collection-like.
   *
   * Note: `List` is a factory function and not a class, and does not use the
   * `new` keyword during construction.
   */
  function List<T>(collection?: Iterable<T> | ArrayLike<T>): List<T>;

  interface List<T> extends Collection.Indexed<T> {
    /**
     * The number of items in this List.
     */
    readonly size: number;

    // Persistent changes

    /**
     * Returns a new List which includes `value` at `index`. If `index` already
     * exists in this List, it will be replaced.
     *
     * `index` may be a negative number, which indexes back from the end of the
     * List. `v.set(-1, "value")` sets the last item in the List.
     *
     * If `index` larger than `size`, the returned List's `size` will be large
     * enough to include the `index`.
     *
     * Note: `set` can be used in `withMutations`.
     */
    set(index: number, value: T): List<T>;

    /**
     * Returns a new List which excludes this `index` and with a size 1 less
     * than this List. Values at indices above `index` are shifted down by 1 to
     * fill the position.
     *
     * This is synonymous with `list.splice(index, 1)`.
     *
     * `index` may be a negative number, which indexes back from the end of the
     * List. `v.delete(-1)` deletes the last item in the List.
     *
     * Note: `delete` cannot be safely used in IE8
     *
     * Since `delete()` re-indexes values, it produces a complete copy, which
     * has `O(N)` complexity.
     *
     * Note: `delete` *cannot* be used in `withMutations`.
     *
     * @alias remove
     */
    delete(index: number): List<T>;
    remove(index: number): List<T>;

    /**
     * Returns a new List with `value` at `index` with a size 1 more than this
     * List. Values at indices above `index` are shifted over by 1.
     *
     * This is synonymous with `list.splice(index, 0, value)`.
     *
     * Since `insert()` re-indexes values, it produces a complete copy, which
     * has `O(N)` complexity.
     *
     * Note: `insert` *cannot* be used in `withMutations`.
     */
    insert(index: number, value: T): List<T>;

    /**
     * Returns a new List with 0 size and no values in constant time.
     *
     * Note: `clear` can be used in `withMutations`.
     */
    clear(): List<T>;

    /**
     * Returns a new List with the provided `values` appended, starting at this
     * List's `size`.
     *
     * Note: `push` can be used in `withMutations`.
     */
    push(...values: Array<T>): List<T>;

    /**
     * Returns a new List with a size ones less than this List, excluding
     * the last index in this List.
     *
     * Note: this differs from `Array#pop` because it returns a new
     * List rather than the removed value. Use `last()` to get the last value
     * in this List.
     *
     * ```js
     * List([ 1, 2, 3, 4 ]).pop()
     * // List[ 1, 2, 3 ]
     * ```
     *
     * Note: `pop` can be used in `withMutations`.
     */
    pop(): List<T>;

    /**
     * Returns a new List with the provided `values` prepended, shifting other
     * values ahead to higher indices.
     *
     * Note: `unshift` can be used in `withMutations`.
     */
    unshift(...values: Array<T>): List<T>;

    /**
     * Returns a new List with a size ones less than this List, excluding
     * the first index in this List, shifting all other values to a lower index.
     *
     * Note: this differs from `Array#shift` because it returns a new
     * List rather than the removed value. Use `first()` to get the first
     * value in this List.
     *
     * Note: `shift` can be used in `withMutations`.
     */
    shift(): List<T>;

    /**
     * Returns a new List with an updated value at `index` with the return
     * value of calling `updater` with the existing value, or `notSetValue` if
     * `index` was not set. If called with a single argument, `updater` is
     * called with the List itself.
     *
     * `index` may be a negative number, which indexes back from the end of the
     * List. `v.update(-1)` updates the last item in the List.
     *
     * This can be very useful as a way to "chain" a normal function into a
     * sequence of methods. RxJS calls this "let" and lodash calls it "thru".
     *
     * For example, to sum a List after mapping and filtering:
     *
     * Note: `update(index)` can be used in `withMutations`.
     *
     * @see `Map#update`
     */
    update(index: number, notSetValue: T, updater: (value: T) => T): this;
    update(
      index: number,
      updater: (value: T | undefined) => T | undefined
    ): this;
    update<R>(updater: (value: this) => R): R;

    /**
     * Returns a new List with size `size`. If `size` is less than this
     * List's size, the new List will exclude values at the higher indices.
     * If `size` is greater than this List's size, the new List will have
     * undefined values for the newly available indices.
     *
     * When building a new List and the final size is known up front, `setSize`
     * used in conjunction with `withMutations` may result in the more
     * performant construction.
     */
    setSize(size: number): List<T>;

    // Deep persistent changes

    /**
     * Returns a new List having set `value` at this `keyPath`. If any keys in
     * `keyPath` do not exist, a new immutable Map will be created at that key.
     *
     * Index numbers are used as keys to determine the path to follow in
     * the List.
     *
     * Plain JavaScript Object or Arrays may be nested within an Immutable.js
     * Collection, and setIn() can update those values as well, treating them
     * immutably by creating new copies of those values with the changes applied.
     *
     * Note: `setIn` can be used in `withMutations`.
     */
    setIn(keyPath: Iterable<unknown>, value: unknown): this;

    /**
     * Returns a new List having removed the value at this `keyPath`. If any
     * keys in `keyPath` do not exist, no change will occur.
     *
     * Plain JavaScript Object or Arrays may be nested within an Immutable.js
     * Collection, and removeIn() can update those values as well, treating them
     * immutably by creating new copies of those values with the changes applied.
     *
     * Note: `deleteIn` *cannot* be safely used in `withMutations`.
     *
     * @alias removeIn
     */
    deleteIn(keyPath: Iterable<unknown>): this;
    removeIn(keyPath: Iterable<unknown>): this;

    /**
     * Note: `updateIn` can be used in `withMutations`.
     *
     * @see `Map#updateIn`
     */
    updateIn(
      keyPath: Iterable<unknown>,
      notSetValue: unknown,
      updater: (value: unknown) => unknown
    ): this;
    updateIn(
      keyPath: Iterable<unknown>,
      updater: (value: unknown) => unknown
    ): this;

    /**
     * Note: `mergeIn` can be used in `withMutations`.
     *
     * @see `Map#mergeIn`
     */
    mergeIn(keyPath: Iterable<unknown>, ...collections: Array<unknown>): this;

    /**
     * Note: `mergeDeepIn` can be used in `withMutations`.
     *
     * @see `Map#mergeDeepIn`
     */
    mergeDeepIn(
      keyPath: Iterable<unknown>,
      ...collections: Array<unknown>
    ): this;

    // Transient changes

    /**
     * Note: Not all methods can be safely used on a mutable collection or within
     * `withMutations`! Check the documentation for each method to see if it
     * allows being used in `withMutations`.
     *
     * @see `Map#withMutations`
     */
    withMutations(mutator: (mutable: this) => unknown): this;

    /**
     * An alternative API for withMutations()
     *
     * Note: Not all methods can be safely used on a mutable collection or within
     * `withMutations`! Check the documentation for each method to see if it
     * allows being used in `withMutations`.
     *
     * @see `Map#asMutable`
     */
    asMutable(): this;

    /**
     * @see `Map#wasAltered`
     */
    wasAltered(): boolean;

    /**
     * @see `Map#asImmutable`
     */
    asImmutable(): this;

    // Sequence algorithms

    /**
     * Returns a new List with other values or collections concatenated to this one.
     *
     * Note: `concat` can be used in `withMutations`.
     *
     * @alias merge
     */
    concat<C>(...valuesOrCollections: Array<Iterable<C> | C>): List<T | C>;
    merge<C>(...collections: Array<Iterable<C>>): List<T | C>;

    /**
     * Returns a new List with values passed through a
     * `mapper` function.
     */
    map<M>(
      mapper: (value: T, key: number, iter: this) => M,
      context?: unknown
    ): List<M>;

    /**
     * Flat-maps the List, returning a new List.
     *
     * Similar to `list.map(...).flatten(true)`.
     */
    flatMap<M>(
      mapper: (value: T, key: number, iter: this) => Iterable<M>,
      context?: unknown
    ): List<M>;

    /**
     * Returns a new List with only the values for which the `predicate`
     * function returns true.
     *
     * Note: `filter()` always returns a new instance, even if it results in
     * not filtering out any values.
     */
    filter<F extends T>(
      predicate: (value: T, index: number, iter: this) => value is F,
      context?: unknown
    ): List<F>;
    filter(
      predicate: (value: T, index: number, iter: this) => unknown,
      context?: unknown
    ): this;

    /**
     * Returns a new List with the values for which the `predicate`
     * function returns false and another for which is returns true.
     */
    partition<F extends T, C>(
      predicate: (this: C, value: T, index: number, iter: this) => value is F,
      context?: C
    ): [List<T>, List<F>];
    partition<C>(
      predicate: (this: C, value: T, index: number, iter: this) => unknown,
      context?: C
    ): [this, this];

    /**
     * Returns a List "zipped" with the provided collection.
     *
     * Like `zipWith`, but using the default `zipper`: creating an `Array`.
     */
    zip<U>(other: Collection<unknown, U>): List<[T, U]>;
    zip<U, V>(
      other: Collection<unknown, U>,
      other2: Collection<unknown, V>
    ): List<[T, U, V]>;
    zip(...collections: Array<Collection<unknown, unknown>>): List<unknown>;

    /**
     * Returns a List "zipped" with the provided collections.
     *
     * Unlike `zip`, `zipAll` continues zipping until the longest collection is
     * exhausted. Missing values from shorter collections are filled with `undefined`.
     *
     * Note: Since zipAll will return a collection as large as the largest
     * input, some results may contain undefined values. TypeScript cannot
     * account for these without cases (as of v2.5).
     */
    zipAll<U>(other: Collection<unknown, U>): List<[T, U]>;
    zipAll<U, V>(
      other: Collection<unknown, U>,
      other2: Collection<unknown, V>
    ): List<[T, U, V]>;
    zipAll(...collections: Array<Collection<unknown, unknown>>): List<unknown>;

    /**
     * Returns a List "zipped" with the provided collections by using a
     * custom `zipper` function.
     */
    zipWith<U, Z>(
      zipper: (value: T, otherValue: U) => Z,
      otherCollection: Collection<unknown, U>
    ): List<Z>;
    zipWith<U, V, Z>(
      zipper: (value: T, otherValue: U, thirdValue: V) => Z,
      otherCollection: Collection<unknown, U>,
      thirdCollection: Collection<unknown, V>
    ): List<Z>;
    zipWith<Z>(
      zipper: (...values: Array<unknown>) => Z,
      ...collections: Array<Collection<unknown, unknown>>
    ): List<Z>;

    /**
     * Returns a new List with its values shuffled thanks to the
     * [Fisher–Yates](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle)
     * algorithm.
     * It uses Math.random, but you can provide your own random number generator.
     */
    shuffle(random?: () => number): this;
  }

  /**
   * Immutable Map is an unordered Collection.Keyed of (key, value) pairs with
   * `O(log32 N)` gets and `O(log32 N)` persistent sets.
   *
   * Iteration order of a Map is undefined, however is stable. Multiple
   * iterations of the same Map will iterate in the same order.
   *
   * Map's keys can be of any type, and use `Immutable.is` to determine key
   * equality. This allows the use of any value (including NaN) as a key.
   *
   * Because `Immutable.is` returns equality based on value semantics, and
   * Immutable collections are treated as values, any Immutable collection may
   * be used as a key.
   *
   * Any JavaScript object may be used as a key, however strict identity is used
   * to evaluate key equality. Two similar looking objects will represent two
   * different keys.
   *
   * Implemented by a hash-array mapped trie.
   */
  namespace Map {
    /**
     * True if the provided value is a Map
     */
    function isMap(maybeMap: unknown): maybeMap is Map<unknown, unknown>;
  }

  /**
   * Creates a new Immutable Map.
   *
   * Created with the same key value pairs as the provided Collection.Keyed or
   * JavaScript Object or expects a Collection of [K, V] tuple entries.
   *
   * Note: `Map` is a factory function and not a class, and does not use the
   * `new` keyword during construction.
   *
   * Keep in mind, when using JS objects to construct Immutable Maps, that
   * JavaScript Object properties are always strings, even if written in a
   * quote-less shorthand, while Immutable Maps accept keys of any type.
   *
   * Property access for JavaScript Objects first converts the key to a string,
   * but since Immutable Map keys can be of any type the argument to `get()` is
   * not altered.
   */
  function Map<K, V>(collection?: Iterable<readonly [K, V]>): Map<K, V>;
  function Map<R extends { [key in PropertyKey]: unknown }>(obj: R): MapOf<R>;
  function Map<V>(obj: { [key: string]: V }): Map<string, V>;
  function Map<K extends string | symbol, V>(obj: { [P in K]?: V }): Map<K, V>;

  /**
   * Represent a Map constructed by an object
   *
   * @ignore
   */
  interface MapOf<R extends { [key in PropertyKey]: unknown }>
    extends Map<keyof R, R[keyof R]> {
    /**
     * Returns the value associated with the provided key, or notSetValue if
     * the Collection does not contain this key.
     *
     * Note: it is possible a key may be associated with an `undefined` value,
     * so if `notSetValue` is not provided and this method returns `undefined`,
     * that does not guarantee the key was not found.
     */
    get<K extends keyof R>(key: K, notSetValue?: unknown): R[K];
    get<NSV>(key: unknown, notSetValue: NSV): NSV;

    // TODO `<const P extends ...>` can be used after dropping support for TypeScript 4.x
    // reference: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#const-type-parameters
    // after this change, `as const` assertions can be remove from the type tests
    getIn<P extends ReadonlyArray<PropertyKey>>(
      searchKeyPath: [...P],
      notSetValue?: unknown
    ): RetrievePath<R, P>;

    set<K extends keyof R>(key: K, value: R[K]): this;

    update(updater: (value: this) => this): this;
    update<K extends keyof R>(key: K, updater: (value: R[K]) => R[K]): this;
    update<K extends keyof R, NSV extends R[K]>(
      key: K,
      notSetValue: NSV,
      updater: (value: R[K]) => R[K]
    ): this;

    // Possible best type is MapOf<Omit<R, K>> but Omit seems to broke other function calls
    // and generate recursion error with other methods (update, merge, etc.) until those functions are defined in MapOf
    delete<K extends keyof R>(
      key: K
    ): Extract<R[K], undefined> extends never ? never : this;
    remove<K extends keyof R>(
      key: K
    ): Extract<R[K], undefined> extends never ? never : this;

    toJS(): { [K in keyof R]: DeepCopy<R[K]> };

    toJSON(): { [K in keyof R]: R[K] };
  }

  // Loosely based off of this work.
  // https://github.com/immutable-js/immutable-js/issues/1462#issuecomment-584123268

  /**
   * @ignore
   * Convert an immutable type to the equivalent plain TS type
   * - MapOf -> object
   * - List -> Array
   */
  type GetNativeType<S> =
    S extends MapOf<infer T> ? T : S extends List<infer I> ? Array<I> : S;

  /** @ignore */
  type Head<T extends ReadonlyArray<unknown>> = T extends [
    infer H,
    ...Array<unknown>,
  ]
    ? H
    : never;
  /** @ignore */
  type Tail<T extends ReadonlyArray<unknown>> = T extends [unknown, ...infer I]
    ? I
    : Array<never>;
  /** @ignore */
  type RetrievePathReducer<
    T,
    C,
    L extends ReadonlyArray<unknown>,
    NT = GetNativeType<T>,
  > =
    // we can not retrieve a path from a primitive type
    T extends string | number | boolean | null | undefined
      ? never
      : C extends keyof NT
        ? L extends [] // L extends [] means we are at the end of the path, lets return the current type
          ? NT[C]
          : // we are not at the end of the path, lets continue with the next key
            RetrievePathReducer<NT[C], Head<L>, Tail<L>>
        : // C is not a "key" of NT, so the path is invalid
          never;

  /** @ignore */
  type RetrievePath<R, P extends ReadonlyArray<unknown>> = P extends []
    ? P
    : RetrievePathReducer<R, Head<P>, Tail<P>>;

  interface Map<K, V> extends Collection.Keyed<K, V> {
    /**
     * The number of entries in this Map.
     */
    readonly size: number;

    // Persistent changes

    /**
     * Returns a new Map also containing the new key, value pair. If an equivalent
     * key already exists in this Map, it will be replaced.
     *
     * Note: `set` can be used in `withMutations`.
     */
    set(key: K, value: V): this;

    /**
     * Returns a new Map which excludes this `key`.
     *
     * Note: `delete` cannot be safely used in IE8, but is provided to mirror
     * the ES6 collection API.
     *
     * Note: `delete` can be used in `withMutations`.
     *
     * @alias remove
     */
    delete(key: K): this;
    remove(key: K): this;

    /**
     * Returns a new Map which excludes the provided `keys`.
     *
     * Note: `deleteAll` can be used in `withMutations`.
     *
     * @alias removeAll
     */
    deleteAll(keys: Iterable<K>): this;
    removeAll(keys: Iterable<K>): this;

    /**
     * Returns a new Map containing no keys or values.
     *
     * Note: `clear` can be used in `withMutations`.
     */
    clear(): this;

    /**
     * Returns a new Map having updated the value at this `key` with the return
     * value of calling `updater` with the existing value.
     *
     * Similar to: `map.set(key, updater(map.get(key)))`.
     *
     * This is most commonly used to call methods on collections within a
     * structure of data. For example, in order to `.push()` onto a nested `List`,
     * `update` and `push` can be used together:
     *
     * When a `notSetValue` is provided, it is provided to the `updater`
     * function when the value at the key does not exist in the Map.
     *
     * However, if the `updater` function returns the same value it was called
     * with, then no change will occur. This is still true if `notSetValue`
     * is provided.
     *
     * For code using ES2015 or later, using `notSetValue` is discourged in
     * favor of function parameter default values. This helps to avoid any
     * potential confusion with identify functions as described above.
     *
     * The previous example behaves differently when written with default values:
     *
     * If no key is provided, then the `updater` function return value is
     * returned as well.
     *
     * This can be very useful as a way to "chain" a normal function into a
     * sequence of methods. RxJS calls this "let" and lodash calls it "thru".
     *
     * For example, to sum the values in a Map
     *
     * Note: `update(key)` can be used in `withMutations`.
     */
    update(key: K, notSetValue: V, updater: (value: V) => V): this;
    update(key: K, updater: (value: V | undefined) => V | undefined): this;
    update<R>(updater: (value: this) => R): R;

    /**
     * Returns a new Map resulting from merging the provided Collections
     * (or JS objects) into this Map. In other words, this takes each entry of
     * each collection and sets it on this Map.
     *
     * Note: Values provided to `merge` are shallowly converted before being
     * merged. No nested values are altered.
     * ```
     *
     * Note: `merge` can be used in `withMutations`.
     *
     * @alias concat
     */
    merge<KC, VC>(
      ...collections: Array<Iterable<[KC, VC]>>
    ): Map<K | KC, Exclude<V, VC> | VC>;
    merge<C>(
      ...collections: Array<{ [key: string]: C }>
    ): Map<K | string, Exclude<V, C> | C>;

    concat<KC, VC>(
      ...collections: Array<Iterable<[KC, VC]>>
    ): Map<K | KC, Exclude<V, VC> | VC>;
    concat<C>(
      ...collections: Array<{ [key: string]: C }>
    ): Map<K | string, Exclude<V, C> | C>;

    /**
     * Like `merge()`, `mergeWith()` returns a new Map resulting from merging
     * the provided Collections (or JS objects) into this Map, but uses the
     * `merger` function for dealing with conflicts.
     *
     * Note: `mergeWith` can be used in `withMutations`.
     */
    mergeWith<KC, VC, VCC>(
      merger: (oldVal: V, newVal: VC, key: K) => VCC,
      ...collections: Array<Iterable<[KC, VC]>>
    ): Map<K | KC, V | VC | VCC>;
    mergeWith<C, CC>(
      merger: (oldVal: V, newVal: C, key: string) => CC,
      ...collections: Array<{ [key: string]: C }>
    ): Map<K | string, V | C | CC>;

    /**
     * Like `merge()`, but when two compatible collections are encountered with
     * the same key, it merges them as well, recursing deeply through the nested
     * data. Two collections are considered to be compatible (and thus will be
     * merged together) if they both fall into one of three categories: keyed
     * (e.g., `Map`s, `Record`s, and objects), indexed (e.g., `List`s and
     * arrays), or set-like (e.g., `Set`s). If they fall into separate
     * categories, `mergeDeep` will replace the existing collection with the
     * collection being merged in. This behavior can be customized by using
     * `mergeDeepWith()`.
     *
     * Note: Indexed and set-like collections are merged using
     * `concat()`/`union()` and therefore do not recurse.
     *
     * Note: `mergeDeep` can be used in `withMutations`.
     */
    mergeDeep<KC, VC>(
      ...collections: Array<Iterable<[KC, VC]>>
    ): Map<K | KC, V | VC>;
    mergeDeep<C>(
      ...collections: Array<{ [key: string]: C }>
    ): Map<K | string, V | C>;

    /**
     * Like `mergeDeep()`, but when two non-collections or incompatible
     * collections are encountered at the same key, it uses the `merger`
     * function to determine the resulting value. Collections are considered
     * incompatible if they fall into separate categories between keyed,
     * indexed, and set-like.
     *
     * Note: `mergeDeepWith` can be used in `withMutations`.
     */
    mergeDeepWith(
      merger: (oldVal: unknown, newVal: unknown, key: unknown) => unknown,
      ...collections: Array<Iterable<[K, V]> | { [key: string]: V }>
    ): this;

    // Deep persistent changes

    /**
     * Returns a new Map having set `value` at this `keyPath`. If any keys in
     * `keyPath` do not exist, a new immutable Map will be created at that key.
     *
     * Plain JavaScript Object or Arrays may be nested within an Immutable.js
     * Collection, and setIn() can update those values as well, treating them
     * immutably by creating new copies of those values with the changes applied.
     *
     * If any key in the path exists but cannot be updated (such as a primitive
     * like number or a custom Object like Date), an error will be thrown.
     *
     * Note: `setIn` can be used in `withMutations`.
     */
    setIn(keyPath: Iterable<unknown>, value: unknown): this;

    /**
     * Returns a new Map having removed the value at this `keyPath`. If any keys
     * in `keyPath` do not exist, no change will occur.
     *
     * Note: `deleteIn` can be used in `withMutations`.
     *
     * @alias removeIn
     */
    deleteIn(keyPath: Iterable<unknown>): this;
    removeIn(keyPath: Iterable<unknown>): this;

    /**
     * Returns a new Map having applied the `updater` to the entry found at the
     * keyPath.
     *
     * This is most commonly used to call methods on collections nested within a
     * structure of data. For example, in order to `.push()` onto a nested `List`,
     * `updateIn` and `push` can be used together:

     *
     * If any keys in `keyPath` do not exist, new Immutable `Map`s will
     * be created at those keys. If the `keyPath` does not already contain a
     * value, the `updater` function will be called with `notSetValue`, if
     * provided, otherwise `undefined`.
     *
     * If the `updater` function returns the same value it was called with, then
     * no change will occur. This is still true if `notSetValue` is provided.
     *
     * For code using ES2015 or later, using `notSetValue` is discourged in
     * favor of function parameter default values. This helps to avoid any
     * potential confusion with identify functions as described above.
     *
     * The previous example behaves differently when written with default values:
     *
     * Plain JavaScript Object or Arrays may be nested within an Immutable.js
     * Collection, and updateIn() can update those values as well, treating them
     * immutably by creating new copies of those values with the changes applied.
     *
     * If any key in the path exists but cannot be updated (such as a primitive
     * like number or a custom Object like Date), an error will be thrown.
     *
     * Note: `updateIn` can be used in `withMutations`.
     */
    updateIn(
      keyPath: Iterable<unknown>,
      notSetValue: unknown,
      updater: (value: unknown) => unknown
    ): this;
    updateIn(
      keyPath: Iterable<unknown>,
      updater: (value: unknown) => unknown
    ): this;

    /**
     * A combination of `updateIn` and `merge`, returning a new Map, but
     * performing the merge at a point arrived at by following the keyPath.
     * In other words, these two lines are equivalent:
     *
     * ```js
     * map.updateIn(['a', 'b', 'c'], abc => abc.merge(y))
     * map.mergeIn(['a', 'b', 'c'], y)
     * ```
     *
     * Note: `mergeIn` can be used in `withMutations`.
     */
    mergeIn(keyPath: Iterable<unknown>, ...collections: Array<unknown>): this;

    /**
     * A combination of `updateIn` and `mergeDeep`, returning a new Map, but
     * performing the deep merge at a point arrived at by following the keyPath.
     * In other words, these two lines are equivalent:
     *
     * ```js
     * map.updateIn(['a', 'b', 'c'], abc => abc.mergeDeep(y))
     * map.mergeDeepIn(['a', 'b', 'c'], y)
     * ```
     *
     * Note: `mergeDeepIn` can be used in `withMutations`.
     */
    mergeDeepIn(
      keyPath: Iterable<unknown>,
      ...collections: Array<unknown>
    ): this;

    // Transient changes

    /**
     * Every time you call one of the above functions, a new immutable Map is
     * created. If a pure function calls a number of these to produce a final
     * return value, then a penalty on performance and memory has been paid by
     * creating all of the intermediate immutable Maps.
     *
     * If you need to apply a series of mutations to produce a new immutable
     * Map, `withMutations()` creates a temporary mutable copy of the Map which
     * can apply mutations in a highly performant manner. In fact, this is
     * exactly how complex mutations like `merge` are done.
     *
     * As an example, this results in the creation of 2, not 4, new Maps:
     *
     * Note: Not all methods can be used on a mutable collection or within
     * `withMutations`! Read the documentation for each method to see if it
     * is safe to use in `withMutations`.
     */
    withMutations(mutator: (mutable: this) => unknown): this;

    /**
     * Another way to avoid creation of intermediate Immutable maps is to create
     * a mutable copy of this collection. Mutable copies *always* return `this`,
     * and thus shouldn't be used for equality. Your function should never return
     * a mutable copy of a collection, only use it internally to create a new
     * collection.
     *
     * If possible, use `withMutations` to work with temporary mutable copies as
     * it provides an easier to use API and considers many common optimizations.
     *
     * Note: if the collection is already mutable, `asMutable` returns itself.
     *
     * Note: Not all methods can be used on a mutable collection or within
     * `withMutations`! Read the documentation for each method to see if it
     * is safe to use in `withMutations`.
     *
     * @see `Map#asImmutable`
     */
    asMutable(): this;

    /**
     * Returns true if this is a mutable copy (see `asMutable()`) and mutative
     * alterations have been applied.
     *
     * @see `Map#asMutable`
     */
    wasAltered(): boolean;

    /**
     * The yin to `asMutable`'s yang. Because it applies to mutable collections,
     * this operation is *mutable* and may return itself (though may not
     * return itself, i.e. if the result is an empty collection). Once
     * performed, the original mutable copy must no longer be mutated since it
     * may be the immutable result.
     *
     * If possible, use `withMutations` to work with temporary mutable copies as
     * it provides an easier to use API and considers many common optimizations.
     *
     * @see `Map#asMutable`
     */
    asImmutable(): this;

    // Sequence algorithms

    /**
     * Returns a new Map with values passed through a
     * `mapper` function.
     *
     *     Map({ a: 1, b: 2 }).map(x => 10 * x)
     *     // Map { a: 10, b: 20 }
     */
    map<M>(
      mapper: (value: V, key: K, iter: this) => M,
      context?: unknown
    ): Map<K, M>;

    /**
     * @see Collection.Keyed.mapKeys
     */
    mapKeys<M>(
      mapper: (key: K, value: V, iter: this) => M,
      context?: unknown
    ): Map<M, V>;

    /**
     * @see Collection.Keyed.mapEntries
     */
    mapEntries<KM, VM>(
      mapper: (
        entry: [K, V],
        index: number,
        iter: this
      ) => [KM, VM] | undefined,
      context?: unknown
    ): Map<KM, VM>;

    /**
     * Flat-maps the Map, returning a new Map.
     *
     * Similar to `data.map(...).flatten(true)`.
     */
    flatMap<KM, VM>(
      mapper: (value: V, key: K, iter: this) => Iterable<[KM, VM]>,
      context?: unknown
    ): Map<KM, VM>;

    /**
     * Returns a new Map with only the entries for which the `predicate`
     * function returns true.
     *
     * Note: `filter()` always returns a new instance, even if it results in
     * not filtering out any values.
     */
    filter<F extends V>(
      predicate: (value: V, key: K, iter: this) => value is F,
      context?: unknown
    ): Map<K, F>;
    filter(
      predicate: (value: V, key: K, iter: this) => unknown,
      context?: unknown
    ): this;

    /**
     * Returns a new Map with the values for which the `predicate`
     * function returns false and another for which is returns true.
     */
    partition<F extends V, C>(
      predicate: (this: C, value: V, key: K, iter: this) => value is F,
      context?: C
    ): [Map<K, V>, Map<K, F>];
    partition<C>(
      predicate: (this: C, value: V, key: K, iter: this) => unknown,
      context?: C
    ): [this, this];

    /**
     * @see Collection.Keyed.flip
     */
    flip(): Map<V, K>;

    /**
     * Returns an OrderedMap of the same type which includes the same entries,
     * stably sorted by using a `comparator`.
     *
     * If a `comparator` is not provided, a default comparator uses `<` and `>`.
     *
     * `comparator(valueA, valueB)`:
     *
     *   * Returns `0` if the elements should not be swapped.
     *   * Returns `-1` (or any negative number) if `valueA` comes before `valueB`
     *   * Returns `1` (or any positive number) if `valueA` comes after `valueB`
     *   * Alternatively, can return a value of the `PairSorting` enum type
     *   * Is pure, i.e. it must always return the same value for the same pair
     *     of values.
     *
     * Note: `sort()` Always returns a new instance, even if the original was
     * already sorted.
     *
     * Note: This is always an eager operation.
     */
    sort(comparator?: Comparator<V>): this & OrderedMap<K, V>;

    /**
     * Like `sort`, but also accepts a `comparatorValueMapper` which allows for
     * sorting by more sophisticated means:
     *
     * Note: `sortBy()` Always returns a new instance, even if the original was
     * already sorted.
     *
     * Note: This is always an eager operation.
     */
    sortBy<C>(
      comparatorValueMapper: (value: V, key: K, iter: this) => C,
      comparator?: (valueA: C, valueB: C) => number
    ): this & OrderedMap<K, V>;
  }

  /**
   * A type of Map that has the additional guarantee that the iteration order of
   * entries will be the order in which they were set().
   *
   * The iteration behavior of OrderedMap is the same as native ES6 Map and
   * JavaScript Object.
   *
   * Note that `OrderedMap` are more expensive than non-ordered `Map` and may
   * consume more memory. `OrderedMap#set` is amortized O(log32 N), but not
   * stable.
   */
  namespace OrderedMap {
    /**
     * True if the provided value is an OrderedMap.
     */
    function isOrderedMap(
      maybeOrderedMap: unknown
    ): maybeOrderedMap is OrderedMap<unknown, unknown>;
  }

  /**
   * Creates a new Immutable OrderedMap.
   *
   * Created with the same key value pairs as the provided Collection.Keyed or
   * JavaScript Object or expects a Collection of [K, V] tuple entries.
   *
   * The iteration order of key-value pairs provided to this constructor will
   * be preserved in the OrderedMap.
   *
   *     let newOrderedMap = OrderedMap({key: "value"})
   *     let newOrderedMap = OrderedMap([["key", "value"]])
   *
   * Note: `OrderedMap` is a factory function and not a class, and does not use
   * the `new` keyword during construction.
   */
  function OrderedMap<K, V>(collection?: Iterable<[K, V]>): OrderedMap<K, V>;
  function OrderedMap<V>(obj: { [key: string]: V }): OrderedMap<string, V>;

  interface OrderedMap<K, V> extends Map<K, V>, OrderedCollection<[K, V]> {
    /**
     * The number of entries in this OrderedMap.
     */
    readonly size: number;

    /**
     * Returns a new OrderedMap also containing the new key, value pair. If an
     * equivalent key already exists in this OrderedMap, it will be replaced
     * while maintaining the existing order.
     *
     * Note: `set` can be used in `withMutations`.
     */
    set(key: K, value: V): this;

    /**
     * Returns a new OrderedMap resulting from merging the provided Collections
     * (or JS objects) into this OrderedMap. In other words, this takes each
     * entry of each collection and sets it on this OrderedMap.
     *
     * Note: Values provided to `merge` are shallowly converted before being
     * merged. No nested values are altered.
     *
     * Note: `merge` can be used in `withMutations`.
     *
     * @alias concat
     */
    merge<KC, VC>(
      ...collections: Array<Iterable<[KC, VC]>>
    ): OrderedMap<K | KC, Exclude<V, VC> | VC>;
    merge<C>(
      ...collections: Array<{ [key: string]: C }>
    ): OrderedMap<K | string, Exclude<V, C> | C>;

    concat<KC, VC>(
      ...collections: Array<Iterable<[KC, VC]>>
    ): OrderedMap<K | KC, Exclude<V, VC> | VC>;
    concat<C>(
      ...collections: Array<{ [key: string]: C }>
    ): OrderedMap<K | string, Exclude<V, C> | C>;

    mergeWith<KC, VC, VCC>(
      merger: (oldVal: V, newVal: VC, key: K) => VCC,
      ...collections: Array<Iterable<[KC, VC]>>
    ): OrderedMap<K | KC, V | VC | VCC>;
    mergeWith<C, CC>(
      merger: (oldVal: V, newVal: C, key: string) => CC,
      ...collections: Array<{ [key: string]: C }>
    ): OrderedMap<K | string, V | C | CC>;

    mergeDeep<KC, VC>(
      ...collections: Array<Iterable<[KC, VC]>>
    ): OrderedMap<K | KC, V | VC>;
    mergeDeep<C>(
      ...collections: Array<{ [key: string]: C }>
    ): OrderedMap<K | string, V | C>;

    // Sequence algorithms

    /**
     * Returns a new OrderedMap with values passed through a
     * `mapper` function.
     *
     *     OrderedMap({ a: 1, b: 2 }).map(x => 10 * x)
     *     // OrderedMap { "a": 10, "b": 20 }
     *
     * Note: `map()` always returns a new instance, even if it produced the same
     * value at every step.
     */
    map<M>(
      mapper: (value: V, key: K, iter: this) => M,
      context?: unknown
    ): OrderedMap<K, M>;

    /**
     * @see Collection.Keyed.mapKeys
     */
    mapKeys<M>(
      mapper: (key: K, value: V, iter: this) => M,
      context?: unknown
    ): OrderedMap<M, V>;

    /**
     * @see Collection.Keyed.mapEntries
     */
    mapEntries<KM, VM>(
      mapper: (
        entry: [K, V],
        index: number,
        iter: this
      ) => [KM, VM] | undefined,
      context?: unknown
    ): OrderedMap<KM, VM>;

    /**
     * Flat-maps the OrderedMap, returning a new OrderedMap.
     *
     * Similar to `data.map(...).flatten(true)`.
     */
    flatMap<KM, VM>(
      mapper: (value: V, key: K, iter: this) => Iterable<[KM, VM]>,
      context?: unknown
    ): OrderedMap<KM, VM>;

    /**
     * Returns a new OrderedMap with only the entries for which the `predicate`
     * function returns true.
     *
     * Note: `filter()` always returns a new instance, even if it results in
     * not filtering out any values.
     */
    filter<F extends V>(
      predicate: (value: V, key: K, iter: this) => value is F,
      context?: unknown
    ): OrderedMap<K, F>;
    filter(
      predicate: (value: V, key: K, iter: this) => unknown,
      context?: unknown
    ): this;

    /**
     * Returns a new OrderedMap with the values for which the `predicate`
     * function returns false and another for which is returns true.
     */
    partition<F extends V, C>(
      predicate: (this: C, value: V, key: K, iter: this) => value is F,
      context?: C
    ): [OrderedMap<K, V>, OrderedMap<K, F>];
    partition<C>(
      predicate: (this: C, value: V, key: K, iter: this) => unknown,
      context?: C
    ): [this, this];

    /**
     * @see Collection.Keyed.flip
     */
    flip(): OrderedMap<V, K>;
  }

  /**
   * A Collection of unique values with `O(log32 N)` adds and has.
   *
   * When iterating a Set, the entries will be (value, value) pairs. Iteration
   * order of a Set is undefined, however is stable. Multiple iterations of the
   * same Set will iterate in the same order.
   *
   * Set values, like Map keys, may be of any type. Equality is determined using
   * `Immutable.is`, enabling Sets to uniquely include other Immutable
   * collections, custom value types, and NaN.
   */
  namespace Set {
    /**
     * True if the provided value is a Set
     */
    function isSet(maybeSet: unknown): maybeSet is Set<unknown>;

    /**
     * Creates a new Set containing `values`.
     */
    function of<T>(...values: Array<T>): Set<T>;

    /**
     * `Set.fromKeys()` creates a new immutable Set containing the keys from
     * this Collection or JavaScript Object.
     */
    function fromKeys<T>(iter: Collection.Keyed<T, unknown>): Set<T>;
    function fromKeys<T>(iter: Collection<T, unknown>): Set<T>;
    function fromKeys(obj: { [key: string]: unknown }): Set<string>;

    /**
     * `Set.intersect()` creates a new immutable Set that is the intersection of
     * a collection of other sets.
     *
     * ```js
     * import { Set } from 'immutable'
     * const intersected = Set.intersect([
     *   Set([ 'a', 'b', 'c' ])
     *   Set([ 'c', 'a', 't' ])
     * ])
     * // Set [ "a", "c" ]
     * ```
     */
    function intersect<T>(sets: Iterable<Iterable<T>>): Set<T>;

    /**
     * `Set.union()` creates a new immutable Set that is the union of a
     * collection of other sets.
     *
     * ```js
     * import { Set } from 'immutable'
     * const unioned = Set.union([
     *   Set([ 'a', 'b', 'c' ])
     *   Set([ 'c', 'a', 't' ])
     * ])
     * // Set [ "a", "b", "c", "t" ]
     * ```
     */
    function union<T>(sets: Iterable<Iterable<T>>): Set<T>;
  }

  /**
   * Create a new immutable Set containing the values of the provided
   * collection-like.
   *
   * Note: `Set` is a factory function and not a class, and does not use the
   * `new` keyword during construction.
   */
  function Set<T>(collection?: Iterable<T> | ArrayLike<T>): Set<T>;

  interface Set<T> extends Collection.Set<T> {
    /**
     * The number of items in this Set.
     */
    readonly size: number;

    // Persistent changes

    /**
     * Returns a new Set which also includes this value.
     *
     * Note: `add` can be used in `withMutations`.
     */
    add(value: T): this;

    /**
     * Returns a new Set which excludes this value.
     *
     * Note: `delete` can be used in `withMutations`.
     *
     * Note: `delete` **cannot** be safely used in IE8, use `remove` if
     * supporting old browsers.
     *
     * @alias remove
     */
    delete(value: T): this;
    remove(value: T): this;

    /**
     * Returns a new Set containing no values.
     *
     * Note: `clear` can be used in `withMutations`.
     */
    clear(): this;

    /**
     * Returns a Set including any value from `collections` that does not already
     * exist in this Set.
     *
     * Note: `union` can be used in `withMutations`.
     * @alias merge
     * @alias concat
     */
    union<C>(...collections: Array<Iterable<C>>): Set<T | C>;
    merge<C>(...collections: Array<Iterable<C>>): Set<T | C>;
    concat<C>(...collections: Array<Iterable<C>>): Set<T | C>;

    /**
     * Returns a Set which has removed any values not also contained
     * within `collections`.
     *
     * Note: `intersect` can be used in `withMutations`.
     */
    intersect(...collections: Array<Iterable<T>>): this;

    /**
     * Returns a Set excluding any values contained within `collections`.
     *
     * Note: `subtract` can be used in `withMutations`.
     */
    subtract(...collections: Array<Iterable<T>>): this;

    // Transient changes

    /**
     * Note: Not all methods can be used on a mutable collection or within
     * `withMutations`! Check the documentation for each method to see if it
     * mentions being safe to use in `withMutations`.
     *
     * @see `Map#withMutations`
     */
    withMutations(mutator: (mutable: this) => unknown): this;

    /**
     * Note: Not all methods can be used on a mutable collection or within
     * `withMutations`! Check the documentation for each method to see if it
     * mentions being safe to use in `withMutations`.
     *
     * @see `Map#asMutable`
     */
    asMutable(): this;

    /**
     * @see `Map#wasAltered`
     */
    wasAltered(): boolean;

    /**
     * @see `Map#asImmutable`
     */
    asImmutable(): this;

    // Sequence algorithms

    /**
     * Returns a new Set with values passed through a
     * `mapper` function.
     *
     *     Set([1,2]).map(x => 10 * x)
     *     // Set [10,20]
     */
    map<M>(
      mapper: (value: T, key: T, iter: this) => M,
      context?: unknown
    ): Set<M>;

    /**
     * Flat-maps the Set, returning a new Set.
     *
     * Similar to `set.map(...).flatten(true)`.
     */
    flatMap<M>(
      mapper: (value: T, key: T, iter: this) => Iterable<M>,
      context?: unknown
    ): Set<M>;

    /**
     * Returns a new Set with only the values for which the `predicate`
     * function returns true.
     *
     * Note: `filter()` always returns a new instance, even if it results in
     * not filtering out any values.
     */
    filter<F extends T>(
      predicate: (value: T, key: T, iter: this) => value is F,
      context?: unknown
    ): Set<F>;
    filter(
      predicate: (value: T, key: T, iter: this) => unknown,
      context?: unknown
    ): this;

    /**
     * Returns a new Set with the values for which the `predicate` function
     * returns false and another for which is returns true.
     */
    partition<F extends T, C>(
      predicate: (this: C, value: T, key: T, iter: this) => value is F,
      context?: C
    ): [Set<T>, Set<F>];
    partition<C>(
      predicate: (this: C, value: T, key: T, iter: this) => unknown,
      context?: C
    ): [this, this];

    /**
     * Returns an OrderedSet of the same type which includes the same entries,
     * stably sorted by using a `comparator`.
     *
     * If a `comparator` is not provided, a default comparator uses `<` and `>`.
     *
     * `comparator(valueA, valueB)`:
     *
     *   * Returns `0` if the elements should not be swapped.
     *   * Returns `-1` (or any negative number) if `valueA` comes before `valueB`
     *   * Returns `1` (or any positive number) if `valueA` comes after `valueB`
     *   * Alternatively, can return a value of the `PairSorting` enum type
     *   * Is pure, i.e. it must always return the same value for the same pair
     *     of values.
     *
     * Note: `sort()` Always returns a new instance, even if the original was
     * already sorted.
     *
     * Note: This is always an eager operation.
     */
    sort(comparator?: Comparator<T>): this & OrderedSet<T>;

    /**
     * Like `sort`, but also accepts a `comparatorValueMapper` which allows for
     * sorting by more sophisticated means:
     *
     * Note: `sortBy()` Always returns a new instance, even if the original was
     * already sorted.
     *
     * Note: This is always an eager operation.
     */
    sortBy<C>(
      comparatorValueMapper: (value: T, key: T, iter: this) => C,
      comparator?: (valueA: C, valueB: C) => number
    ): this & OrderedSet<T>;
  }

  /**
   * A type of Set that has the additional guarantee that the iteration order of
   * values will be the order in which they were `add`ed.
   *
   * The iteration behavior of OrderedSet is the same as native ES6 Set.
   *
   * Note that `OrderedSet` are more expensive than non-ordered `Set` and may
   * consume more memory. `OrderedSet#add` is amortized O(log32 N), but not
   * stable.
   */
  namespace OrderedSet {
    /**
     * True if the provided value is an OrderedSet.
     */
    function isOrderedSet(
      maybeOrderedSet: unknown
    ): maybeOrderedSet is OrderedSet<unknown>;

    /**
     * Creates a new OrderedSet containing `values`.
     */
    function of<T>(...values: Array<T>): OrderedSet<T>;

    /**
     * `OrderedSet.fromKeys()` creates a new immutable OrderedSet containing
     * the keys from this Collection or JavaScript Object.
     */
    function fromKeys<T>(iter: Collection.Keyed<T, unknown>): OrderedSet<T>;
    function fromKeys<T>(iter: Collection<T, unknown>): OrderedSet<T>;
    function fromKeys(obj: { [key: string]: unknown }): OrderedSet<string>;
  }

  /**
   * Create a new immutable OrderedSet containing the values of the provided
   * collection-like.
   *
   * Note: `OrderedSet` is a factory function and not a class, and does not use
   * the `new` keyword during construction.
   */
  function OrderedSet<T>(
    collection?: Iterable<T> | ArrayLike<T>
  ): OrderedSet<T>;

  interface OrderedSet<T> extends Set<T>, OrderedCollection<T> {
    /**
     * The number of items in this OrderedSet.
     */
    readonly size: number;

    /**
     * Returns an OrderedSet including any value from `collections` that does
     * not already exist in this OrderedSet.
     *
     * Note: `union` can be used in `withMutations`.
     * @alias merge
     * @alias concat
     */
    union<C>(...collections: Array<Iterable<C>>): OrderedSet<T | C>;
    merge<C>(...collections: Array<Iterable<C>>): OrderedSet<T | C>;
    concat<C>(...collections: Array<Iterable<C>>): OrderedSet<T | C>;

    // Sequence algorithms

    /**
     * Returns a new Set with values passed through a
     * `mapper` function.
     *
     *     OrderedSet([ 1, 2 ]).map(x => 10 * x)
     *     // OrderedSet [10, 20]
     */
    map<M>(
      mapper: (value: T, key: T, iter: this) => M,
      context?: unknown
    ): OrderedSet<M>;

    /**
     * Flat-maps the OrderedSet, returning a new OrderedSet.
     *
     * Similar to `set.map(...).flatten(true)`.
     */
    flatMap<M>(
      mapper: (value: T, key: T, iter: this) => Iterable<M>,
      context?: unknown
    ): OrderedSet<M>;

    /**
     * Returns a new OrderedSet with only the values for which the `predicate`
     * function returns true.
     *
     * Note: `filter()` always returns a new instance, even if it results in
     * not filtering out any values.
     */
    filter<F extends T>(
      predicate: (value: T, key: T, iter: this) => value is F,
      context?: unknown
    ): OrderedSet<F>;
    filter(
      predicate: (value: T, key: T, iter: this) => unknown,
      context?: unknown
    ): this;

    /**
     * Returns a new OrderedSet with the values for which the `predicate`
     * function returns false and another for which is returns true.
     */
    partition<F extends T, C>(
      predicate: (this: C, value: T, key: T, iter: this) => value is F,
      context?: C
    ): [OrderedSet<T>, OrderedSet<F>];
    partition<C>(
      predicate: (this: C, value: T, key: T, iter: this) => unknown,
      context?: C
    ): [this, this];

    /**
     * Returns an OrderedSet of the same type "zipped" with the provided
     * collections.
     *
     * Like `zipWith`, but using the default `zipper`: creating an `Array`.
     *
     * ```js
     * const a = OrderedSet([ 1, 2, 3 ])
     * const b = OrderedSet([ 4, 5, 6 ])
     * const c = a.zip(b)
     * // OrderedSet [ [ 1, 4 ], [ 2, 5 ], [ 3, 6 ] ]
     * ```
     */
    zip<U>(other: Collection<unknown, U>): OrderedSet<[T, U]>;
    zip<U, V>(
      other1: Collection<unknown, U>,
      other2: Collection<unknown, V>
    ): OrderedSet<[T, U, V]>;
    zip(
      ...collections: Array<Collection<unknown, unknown>>
    ): OrderedSet<unknown>;

    /**
     * Returns a OrderedSet of the same type "zipped" with the provided
     * collections.
     *
     * Unlike `zip`, `zipAll` continues zipping until the longest collection is
     * exhausted. Missing values from shorter collections are filled with `undefined`.
     *
     * ```js
     * const a = OrderedSet([ 1, 2 ]);
     * const b = OrderedSet([ 3, 4, 5 ]);
     * const c = a.zipAll(b); // OrderedSet [ [ 1, 3 ], [ 2, 4 ], [ undefined, 5 ] ]
     * ```
     *
     * Note: Since zipAll will return a collection as large as the largest
     * input, some results may contain undefined values. TypeScript cannot
     * account for these without cases (as of v2.5).
     */
    zipAll<U>(other: Collection<unknown, U>): OrderedSet<[T, U]>;
    zipAll<U, V>(
      other1: Collection<unknown, U>,
      other2: Collection<unknown, V>
    ): OrderedSet<[T, U, V]>;
    zipAll(
      ...collections: Array<Collection<unknown, unknown>>
    ): OrderedSet<unknown>;

    /**
     * Returns an OrderedSet of the same type "zipped" with the provided
     * collections by using a custom `zipper` function.
     *
     * @see Seq.Indexed.zipWith
     */
    zipWith<U, Z>(
      zipper: (value: T, otherValue: U) => Z,
      otherCollection: Collection<unknown, U>
    ): OrderedSet<Z>;
    zipWith<U, V, Z>(
      zipper: (value: T, otherValue: U, thirdValue: V) => Z,
      otherCollection: Collection<unknown, U>,
      thirdCollection: Collection<unknown, V>
    ): OrderedSet<Z>;
    zipWith<Z>(
      zipper: (...values: Array<unknown>) => Z,
      ...collections: Array<Collection<unknown, unknown>>
    ): OrderedSet<Z>;
  }

  /**
   * Stacks are indexed collections which support very efficient O(1) addition
   * and removal from the front using `unshift(v)` and `shift()`.
   *
   * For familiarity, Stack also provides `push(v)`, `pop()`, and `peek()`, but
   * be aware that they also operate on the front of the list, unlike List or
   * a JavaScript Array.
   *
   * Note: `reverse()` or any inherent reverse traversal (`reduceRight`,
   * `lastIndexOf`, etc.) is not efficient with a Stack.
   *
   * Stack is implemented with a Single-Linked List.
   */
  namespace Stack {
    /**
     * True if the provided value is a Stack
     */
    function isStack(maybeStack: unknown): maybeStack is Stack<unknown>;

    /**
     * Creates a new Stack containing `values`.
     */
    function of<T>(...values: Array<T>): Stack<T>;
  }

  /**
   * Create a new immutable Stack containing the values of the provided
   * collection-like.
   *
   * The iteration order of the provided collection is preserved in the
   * resulting `Stack`.
   *
   * Note: `Stack` is a factory function and not a class, and does not use the
   * `new` keyword during construction.
   */
  function Stack<T>(collection?: Iterable<T> | ArrayLike<T>): Stack<T>;

  interface Stack<T> extends Collection.Indexed<T> {
    /**
     * The number of items in this Stack.
     */
    readonly size: number;

    // Reading values

    /**
     * Alias for `Stack.first()`.
     */
    peek(): T | undefined;

    // Persistent changes

    /**
     * Returns a new Stack with 0 size and no values.
     *
     * Note: `clear` can be used in `withMutations`.
     */
    clear(): Stack<T>;

    /**
     * Returns a new Stack with the provided `values` prepended, shifting other
     * values ahead to higher indices.
     *
     * This is very efficient for Stack.
     *
     * Note: `unshift` can be used in `withMutations`.
     */
    unshift(...values: Array<T>): Stack<T>;

    /**
     * Like `Stack#unshift`, but accepts a collection rather than varargs.
     *
     * Note: `unshiftAll` can be used in `withMutations`.
     */
    unshiftAll(iter: Iterable<T>): Stack<T>;

    /**
     * Returns a new Stack with a size ones less than this Stack, excluding
     * the first item in this Stack, shifting all other values to a lower index.
     *
     * Note: this differs from `Array#shift` because it returns a new
     * Stack rather than the removed value. Use `first()` or `peek()` to get the
     * first value in this Stack.
     *
     * Note: `shift` can be used in `withMutations`.
     */
    shift(): Stack<T>;

    /**
     * Alias for `Stack#unshift` and is not equivalent to `List#push`.
     */
    push(...values: Array<T>): Stack<T>;

    /**
     * Alias for `Stack#unshiftAll`.
     */
    pushAll(iter: Iterable<T>): Stack<T>;

    /**
     * Alias for `Stack#shift` and is not equivalent to `List#pop`.
     */
    pop(): Stack<T>;

    // Transient changes

    /**
     * Note: Not all methods can be used on a mutable collection or within
     * `withMutations`! Check the documentation for each method to see if it
     * mentions being safe to use in `withMutations`.
     *
     * @see `Map#withMutations`
     */
    withMutations(mutator: (mutable: this) => unknown): this;

    /**
     * Note: Not all methods can be used on a mutable collection or within
     * `withMutations`! Check the documentation for each method to see if it
     * mentions being safe to use in `withMutations`.
     *
     * @see `Map#asMutable`
     */
    asMutable(): this;

    /**
     * @see `Map#wasAltered`
     */
    wasAltered(): boolean;

    /**
     * @see `Map#asImmutable`
     */
    asImmutable(): this;

    // Sequence algorithms

    /**
     * Returns a new Stack with other collections concatenated to this one.
     */
    concat<C>(...valuesOrCollections: Array<Iterable<C> | C>): Stack<T | C>;

    /**
     * Returns a new Stack with values passed through a
     * `mapper` function.
     *
     *     Stack([ 1, 2 ]).map(x => 10 * x)
     *     // Stack [ 10, 20 ]
     *
     * Note: `map()` always returns a new instance, even if it produced the same
     * value at every step.
     */
    map<M>(
      mapper: (value: T, key: number, iter: this) => M,
      context?: unknown
    ): Stack<M>;

    /**
     * Flat-maps the Stack, returning a new Stack.
     *
     * Similar to `stack.map(...).flatten(true)`.
     */
    flatMap<M>(
      mapper: (value: T, key: number, iter: this) => Iterable<M>,
      context?: unknown
    ): Stack<M>;

    /**
     * Returns a new Set with only the values for which the `predicate`
     * function returns true.
     *
     * Note: `filter()` always returns a new instance, even if it results in
     * not filtering out any values.
     */
    filter<F extends T>(
      predicate: (value: T, index: number, iter: this) => value is F,
      context?: unknown
    ): Set<F>;
    filter(
      predicate: (value: T, index: number, iter: this) => unknown,
      context?: unknown
    ): this;

    /**
     * Returns a Stack "zipped" with the provided collections.
     *
     * Like `zipWith`, but using the default `zipper`: creating an `Array`.
     *
     * ```js
     * const a = Stack([ 1, 2, 3 ]);
     * const b = Stack([ 4, 5, 6 ]);
     * const c = a.zip(b); // Stack [ [ 1, 4 ], [ 2, 5 ], [ 3, 6 ] ]
     * ```
     */
    zip<U>(other: Collection<unknown, U>): Stack<[T, U]>;
    zip<U, V>(
      other: Collection<unknown, U>,
      other2: Collection<unknown, V>
