From 6daec691befba828d396956ee2e0ac707b58bd2a Mon Sep 17 00:00:00 2001 From: Julien Deniau Date: Thu, 19 Jun 2025 08:27:51 +0000 Subject: [PATCH 1/4] Migrate Range file to TS --- src/Collection.js | 6 +++ src/Iterator.ts | 79 ++++++++++++++++++++++++-------------- src/{Range.js => Range.ts} | 66 ++++++++++++++++++++++--------- src/TrieUtils.ts | 20 ++++++---- src/toJS.ts | 2 +- 5 files changed, 117 insertions(+), 56 deletions(-) rename src/{Range.js => Range.ts} (61%) diff --git a/src/Collection.js b/src/Collection.js index 42c93406b3..7eebd8f7a3 100644 --- a/src/Collection.js +++ b/src/Collection.js @@ -5,6 +5,12 @@ import { isIndexed } from './predicates/isIndexed'; import { isKeyed } from './predicates/isKeyed'; export const Collection = (value) => (isCollection(value) ? value : Seq(value)); + +/** + * @template K + * @template V + * @extends {import('../type-definitions/immutable').ValueObject} + */ export class CollectionImpl {} export const KeyedCollection = (value) => diff --git a/src/Iterator.ts b/src/Iterator.ts index 8de2a8062b..953b56e799 100644 --- a/src/Iterator.ts +++ b/src/Iterator.ts @@ -2,19 +2,19 @@ export const ITERATE_KEYS = 0; export const ITERATE_VALUES = 1; export const ITERATE_ENTRIES = 2; -type IteratorType = +export type IteratorType = | typeof ITERATE_KEYS | typeof ITERATE_VALUES | typeof ITERATE_ENTRIES; -export class Iterator implements globalThis.Iterator { +export class Iterator implements globalThis.Iterator { static KEYS = ITERATE_KEYS; static VALUES = ITERATE_VALUES; static ENTRIES = ITERATE_ENTRIES; - declare next: () => IteratorResult; + declare next: () => IteratorResult; - constructor(next: () => IteratorResult) { + constructor(next: () => IteratorResult) { if (next) { // Map extends Iterator and has a `next` method, do not erase it in that case. We could have checked `if (next && !this.next)` too. this.next = next; @@ -41,42 +41,65 @@ export class Iterator implements globalThis.Iterator { export function iteratorValue( type: IteratorType, k: K, - v?: undefined, - iteratorResult?: IteratorResult -): IteratorResult | undefined; + v: undefined, + iteratorResult?: IteratorYieldResult +): IteratorYieldResult; export function iteratorValue( type: IteratorType, k: K, v: V, - iteratorResult?: IteratorResult -): IteratorResult | undefined; + iteratorResult?: IteratorYieldResult +): IteratorYieldResult; export function iteratorValue( type: typeof ITERATE_ENTRIES, k: K, - v?: V, - iteratorResult?: IteratorResult<[K, V]> -): IteratorResult<[K, V]> | undefined; + v: V, + iteratorResult?: IteratorYieldResult<[K, V]> +): IteratorYieldResult<[K, V]>; export function iteratorValue( type: IteratorType, k: K, - v?: V, + v: V, iteratorResult?: - | IteratorResult - | IteratorResult - | IteratorResult<[K, V]> -): IteratorResult | IteratorResult | IteratorResult<[K, V]> | undefined { - const value = - type === ITERATE_KEYS ? k : type === ITERATE_VALUES ? v : [k, v]; + | IteratorYieldResult + | IteratorYieldResult + | IteratorYieldResult<[K, V]> +): IteratorYieldResult { + const value = getValueFromType(type, k, v); + // type === ITERATE_KEYS ? k : type === ITERATE_VALUES ? v : [k, v]; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions -- TODO enable eslint here - iteratorResult - ? (iteratorResult.value = value) - : (iteratorResult = { - // @ts-expect-error ensure value is not undefined - value: value, - done: false, - }); - - return iteratorResult; + + if (iteratorResult) { + iteratorResult.value = value; + + return iteratorResult; + } + + return { + value: value, + done: false, + }; +} + +function getValueFromType(type: typeof ITERATE_KEYS, k: K, v: V): K; +function getValueFromType( + type: typeof ITERATE_VALUES, + k: K, + v: V +): V | undefined; +function getValueFromType( + type: typeof ITERATE_ENTRIES, + k: K, + v: V +): [K, V] | undefined; +function getValueFromType(type: IteratorType, k: K, v: V): K | V | [K, V]; +function getValueFromType( + type: IteratorType, + k: K, + v: V +): K | V | [K, V] { + return type === ITERATE_KEYS ? k : type === ITERATE_VALUES ? v : [k, v]; } export function iteratorDone(): IteratorReturnResult { diff --git a/src/Range.js b/src/Range.ts similarity index 61% rename from src/Range.js rename to src/Range.ts index 8947d91be5..fd33aedca8 100644 --- a/src/Range.js +++ b/src/Range.ts @@ -1,15 +1,28 @@ -import { Iterator, iteratorValue, iteratorDone } from './Iterator'; +import type { Seq } from '../type-definitions/immutable'; +import { + Iterator, + iteratorValue, + iteratorDone, + type IteratorType, +} from './Iterator'; import { IndexedSeqImpl } from './Seq'; import { wrapIndex, wholeSlice, resolveBegin, resolveEnd } from './TrieUtils'; import deepEqual from './utils/deepEqual'; import invariant from './utils/invariant'; /** - * Returns a lazy seq of nums from start (inclusive) to end - * (exclusive), by step, where start defaults to 0, step to 1, and end to - * infinity. When start is equal to end, returns empty list. + * Returns a `Seq.Indexed` of numbers from `start` (inclusive) to `end` + * (exclusive), by `step`, where `start` defaults to 0, `step` to 1, and `end` to + * infinity. When `start` is equal to `end`, returns empty range. + * + * Note: `Range` is a factory function and not a class, and does not use the + * `new` keyword during construction. */ -export const Range = (start, end, step = 1) => { +export const Range = ( + start: number, + end: number, + step: number = 1 +): RangeImpl => { invariant(step !== 0, 'Cannot step a Range by 0'); invariant( start !== undefined, @@ -30,8 +43,13 @@ export const Range = (start, end, step = 1) => { } return new RangeImpl(start, end, step, size); }; -export class RangeImpl extends IndexedSeqImpl { - constructor(start, end, step, size) { + +export class RangeImpl extends IndexedSeqImpl implements Seq.Indexed { + private _start: number; + private _end: number; + private _step: number; + + constructor(start: number, end: number, step: number, size: number) { super(); this._start = start; @@ -40,19 +58,22 @@ export class RangeImpl extends IndexedSeqImpl { this.size = size; } - toString() { + override toString(): string { return this.size === 0 ? 'Range []' : `Range [ ${this._start}...${this._end}${this._step !== 1 ? ' by ' + this._step : ''} ]`; } - get(index, notSetValue) { + get(index: number, notSetValue: NSV): number | NSV; + get(index: number): number | undefined; + get(index: number, notSetValue?: NSV): number | NSV | undefined { + // @ts-expect-error Issue with the mixin not understood by TypeScript return this.has(index) ? this._start + wrapIndex(this, index) * this._step : notSetValue; } - includes(searchValue) { + includes(searchValue: number): boolean { const possibleIndex = (searchValue - this._start) / this._step; return ( possibleIndex >= 0 && @@ -61,7 +82,8 @@ export class RangeImpl extends IndexedSeqImpl { ); } - slice(begin, end) { + // @ts-expect-error TypeScript does not understand the mixin + slice(begin?: number | undefined, end?: number | undefined): RangeImpl { if (wholeSlice(begin, end, this.size)) { return this; } @@ -77,7 +99,7 @@ export class RangeImpl extends IndexedSeqImpl { ); } - indexOf(searchValue) { + indexOf(searchValue: number): number { const offsetValue = searchValue - this._start; if (offsetValue % this._step === 0) { const index = offsetValue / this._step; @@ -88,11 +110,14 @@ export class RangeImpl extends IndexedSeqImpl { return -1; } - lastIndexOf(searchValue) { + lastIndexOf(searchValue: number): number { return this.indexOf(searchValue); } - __iterate(fn, reverse) { + override __iterate( + fn: (value: number, index: number, iter: this) => boolean, + reverse: boolean = false + ): number { const size = this.size; const step = this._step; let value = reverse ? this._start + (size - 1) * step : this._start; @@ -106,12 +131,15 @@ export class RangeImpl extends IndexedSeqImpl { return i; } - __iterator(type, reverse) { + override __iterator( + type: IteratorType, + reverse: boolean = false + ): Iterator { const size = this.size; const step = this._step; let value = reverse ? this._start + (size - 1) * step : this._start; let i = 0; - return new Iterator(() => { + return new Iterator(() => { if (i === size) { return iteratorDone(); } @@ -121,8 +149,8 @@ export class RangeImpl extends IndexedSeqImpl { }); } - equals(other) { - return other instanceof Range + equals(other: unknown): boolean { + return other instanceof RangeImpl ? this._start === other._start && this._end === other._end && this._step === other._step @@ -130,4 +158,4 @@ export class RangeImpl extends IndexedSeqImpl { } } -let EMPTY_RANGE; +let EMPTY_RANGE: RangeImpl | undefined; diff --git a/src/TrieUtils.ts b/src/TrieUtils.ts index d5c75c179b..9a1c6f9580 100644 --- a/src/TrieUtils.ts +++ b/src/TrieUtils.ts @@ -1,4 +1,4 @@ -import type { Collection } from '../type-definitions/immutable'; +import type { CollectionImpl } from './Collection'; // Used for setting prototype methods that IE8 chokes on. export const DELETE = 'delete'; @@ -30,7 +30,7 @@ export function SetRef(ref: Ref): void { // the return of any subsequent call of this function. export function OwnerID() {} -export function ensureSize(iter: Collection): number { +export function ensureSize(iter: CollectionImpl): number { // @ts-expect-error size should exists on Collection if (iter.size === undefined) { // @ts-expect-error size should exists on Collection, __iterate does exist on Collection @@ -41,7 +41,7 @@ export function ensureSize(iter: Collection): number { } export function wrapIndex( - iter: Collection, + iter: CollectionImpl, index: number ): number { // This implements "is array index" which the ECMAString spec defines as: @@ -65,24 +65,28 @@ export function returnTrue(): true { return true; } -export function wholeSlice(begin: number, end: number, size: number): boolean { +export function wholeSlice( + begin: number | undefined, + end: number | undefined, + size: number +): boolean { return ( ((begin === 0 && !isNeg(begin)) || - (size !== undefined && begin <= -size)) && + (size !== undefined && (begin ?? 0) <= -size)) && (end === undefined || (size !== undefined && end >= size)) ); } -export function resolveBegin(begin: number, size: number): number { +export function resolveBegin(begin: number | undefined, size: number): number { return resolveIndex(begin, size, 0); } -export function resolveEnd(end: number, size: number): number { +export function resolveEnd(end: number | undefined, size: number): number { return resolveIndex(end, size, size); } function resolveIndex( - index: number, + index: number | undefined, size: number, defaultIndex: number ): number { diff --git a/src/toJS.ts b/src/toJS.ts index 6d839f2a96..38370d3727 100644 --- a/src/toJS.ts +++ b/src/toJS.ts @@ -6,7 +6,7 @@ import { isKeyed } from './predicates/isKeyed'; import isDataStructure from './utils/isDataStructure'; export function toJS( - value: CollectionImpl | RecordImpl + value: CollectionImpl | RecordImpl ): Array | { [key: string]: unknown }; export function toJS(value: unknown): unknown; export function toJS( From 4b62a1580e965671aef8a60d91ebb6634f6db729 Mon Sep 17 00:00:00 2001 From: Julien Deniau Date: Mon, 23 Jun 2025 06:42:41 +0000 Subject: [PATCH 2/4] Start migration Collection to TS --- src/Collection.js | 33 ------------------ src/Collection.ts | 85 ++++++++++++++++++++++++++++++++++++++++++++++ src/ValueObject.ts | 36 ++++++++++++++++++++ 3 files changed, 121 insertions(+), 33 deletions(-) delete mode 100644 src/Collection.js create mode 100644 src/Collection.ts create mode 100644 src/ValueObject.ts diff --git a/src/Collection.js b/src/Collection.js deleted file mode 100644 index 7eebd8f7a3..0000000000 --- a/src/Collection.js +++ /dev/null @@ -1,33 +0,0 @@ -import { IndexedSeq, KeyedSeq, Seq, SetSeq } from './Seq'; -import { isAssociative } from './predicates/isAssociative'; -import { isCollection } from './predicates/isCollection'; -import { isIndexed } from './predicates/isIndexed'; -import { isKeyed } from './predicates/isKeyed'; - -export const Collection = (value) => (isCollection(value) ? value : Seq(value)); - -/** - * @template K - * @template V - * @extends {import('../type-definitions/immutable').ValueObject} - */ -export class CollectionImpl {} - -export const KeyedCollection = (value) => - isKeyed(value) ? value : KeyedSeq(value); - -export class KeyedCollectionImpl extends CollectionImpl {} - -export const IndexedCollection = (value) => - isIndexed(value) ? value : IndexedSeq(value); - -export class IndexedCollectionImpl extends CollectionImpl {} - -export const SetCollection = (value) => - isCollection(value) && !isAssociative(value) ? value : SetSeq(value); - -export class SetCollectionImpl extends CollectionImpl {} - -Collection.Keyed = KeyedCollectionImpl; -Collection.Indexed = IndexedCollectionImpl; -Collection.Set = SetCollectionImpl; diff --git a/src/Collection.ts b/src/Collection.ts new file mode 100644 index 0000000000..25f3c94e27 --- /dev/null +++ b/src/Collection.ts @@ -0,0 +1,85 @@ +import { IndexedSeq, KeyedSeq, KeyedSeqImpl, Seq, SetSeq } from './Seq'; +import type ValueObject from './ValueObject'; +import { isAssociative } from './predicates/isAssociative'; +import { isCollection } from './predicates/isCollection'; +import { isIndexed } from './predicates/isIndexed'; +import { isKeyed } from './predicates/isKeyed'; + +export function Collection>( + collection: I +): I; +export function Collection( + collection: Iterable | ArrayLike +): IndexedCollectionImpl; +export function Collection(obj: { + [key: string]: V; +}): KeyedCollectionImpl; +export function Collection( + value: never +): CollectionImpl; +export function Collection(value: unknown): CollectionImpl { + return isCollection(value) ? value : Seq(value); +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export class CollectionImpl implements ValueObject { + declare equals: (other: unknown) => boolean; + + declare hashCode: () => number; +} + +/** + * Always returns a Seq.Keyed, if input is not keyed, expects an + * collection of [K, V] tuples. + * + * Note: `Seq.Keyed` is a conversion function and not a class, and does not + * use the `new` keyword during construction. + */ +export function Keyed( + collection?: Iterable<[K, V]> +): KeyedCollectionImpl; +export function Keyed(obj: { + [key: string]: V; +}): KeyedCollectionImpl; +export function Keyed(value: unknown): KeyedCollectionImpl { + return isKeyed(value) ? value : KeyedSeq(value); +} + +export class KeyedCollectionImpl extends CollectionImpl {} + +export function IndexedCollection(value) { + return isIndexed(value) ? value : IndexedSeq(value); +} + +/** + * Interface representing all oredered collections. + * This includes `List`, `Stack`, `Map`, `OrderedMap`, `Set`, and `OrderedSet`. + * return of `isOrdered()` return true in that case. + */ +interface OrderedCollection { + /** + * Shallowly converts this collection to an Array. + */ + toArray(): Array; + + [Symbol.iterator](): IterableIterator; +} + +export class IndexedCollectionImpl + extends CollectionImpl + implements OrderedCollection +{ + declare toArray: () => T[]; + + declare [Symbol.iterator]: () => IterableIterator; +} + +export function SetCollection(value) { + return isCollection(value) && !isAssociative(value) ? value : SetSeq(value); +} + +export class SetCollectionImpl extends CollectionImpl {} + +Collection.Keyed = Keyed; +Collection.Indexed = IndexedCollection; +Collection.Set = SetCollection; diff --git a/src/ValueObject.ts b/src/ValueObject.ts new file mode 100644 index 0000000000..edace204d2 --- /dev/null +++ b/src/ValueObject.ts @@ -0,0 +1,36 @@ +/** + * The interface to fulfill to qualify as a Value Object. + */ +export default interface ValueObject { + /** + * True if this and the other Collection have value equality, as defined + * by `Immutable.is()`. + * + * Note: This is equivalent to `Immutable.is(this, other)`, but provided to + * allow for chained expressions. + */ + equals(other: unknown): boolean; + + /** + * Computes and returns the hashed identity for this Collection. + * + * The `hashCode` of a Collection is used to determine potential equality, + * and is used when adding this to a `Set` or as a key in a `Map`, enabling + * lookup via a different instance. + * + * Note: hashCode() MUST return a Uint32 number. The easiest way to + * guarantee this is to return `myHash | 0` from a custom implementation. + * + * If two values have the same `hashCode`, they are [not guaranteed + * to be equal][Hash Collision]. If two values have different `hashCode`s, + * they must not be equal. + * + * Note: `hashCode()` is not guaranteed to always be called before + * `equals()`. Most but not all Immutable.js collections use hash codes to + * organize their internal data structures, while all Immutable.js + * collections use equality during lookups. + * + * [Hash Collision]: https://en.wikipedia.org/wiki/Collision_(computer_science) + */ + hashCode(): number; +} From d7a41f78eda15f7d54e6f9b6ea518f1671d38bf2 Mon Sep 17 00:00:00 2001 From: Julien Deniau Date: Thu, 26 Jun 2025 22:42:35 +0000 Subject: [PATCH 3/4] Migrate some function from the mixin to the main class --- src/Collection.ts | 84 +++++++++++++++++++++++++++++---- src/CollectionImpl.js | 84 ++++----------------------------- src/Iterator.ts | 2 - src/Range.ts | 4 +- src/TrieUtils.ts | 4 +- src/predicates/isAssociative.ts | 6 +-- src/predicates/isCollection.ts | 4 +- src/predicates/isIndexed.ts | 4 +- src/predicates/isKeyed.ts | 4 +- src/predicates/isOrdered.ts | 3 ++ src/toJS.ts | 3 +- src/utils/deepEqual.ts | 20 +++----- src/utils/hashCollection.ts | 49 +++++++++++++++++++ 13 files changed, 153 insertions(+), 118 deletions(-) create mode 100644 src/utils/hashCollection.ts diff --git a/src/Collection.ts b/src/Collection.ts index 25f3c94e27..1d1aae6ddf 100644 --- a/src/Collection.ts +++ b/src/Collection.ts @@ -1,9 +1,13 @@ -import { IndexedSeq, KeyedSeq, KeyedSeqImpl, Seq, SetSeq } from './Seq'; +import { ITERATE_ENTRIES, type IteratorType } from './Iterator'; +import { IndexedSeq, KeyedSeq, Seq, SetSeq } from './Seq'; import type ValueObject from './ValueObject'; import { isAssociative } from './predicates/isAssociative'; import { isCollection } from './predicates/isCollection'; import { isIndexed } from './predicates/isIndexed'; import { isKeyed } from './predicates/isKeyed'; +import assertNotInfinite from './utils/assertNotInfinite'; +import deepEqual from './utils/deepEqual'; +import { hashCollection } from './utils/hashCollection'; export function Collection>( collection: I @@ -21,11 +25,65 @@ export function Collection(value: unknown): CollectionImpl { return isCollection(value) ? value : Seq(value); } -// eslint-disable-next-line @typescript-eslint/no-unused-vars export class CollectionImpl implements ValueObject { - declare equals: (other: unknown) => boolean; + private __hash: number | undefined; - declare hashCode: () => number; + size: number = 0; + + equals(other: unknown): boolean { + return deepEqual(this, other); + } + + hashCode() { + return this.__hash || (this.__hash = hashCollection(this)); + } + + every( + predicate: (value: V, key: K, iter: this) => boolean, + context?: CollectionImpl + ): boolean { + assertNotInfinite(this.size); + let returnValue = true; + this.__iterate((v, k, c) => { + if (!predicate.call(context, v, k, c)) { + returnValue = false; + return false; + } + }); + return returnValue; + } + + entries() { + return this.__iterator(ITERATE_ENTRIES); + } + + __iterate( + fn: (value: V, index: K, iter: this) => boolean, + reverse?: boolean + ): number; + __iterate( + fn: (value: V, index: K, iter: this) => void, + reverse?: boolean + ): void; + __iterate( + fn: (value: V, index: K, iter: this) => boolean | void, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + reverse: boolean = false + ): number | void { + throw new Error( + 'CollectionImpl does not implement __iterate. Use a subclass instead.' + ); + } + + __iterator( + type: IteratorType, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + reverse: boolean = false + ): Iterator { + throw new Error( + 'CollectionImpl does not implement __iterator. Use a subclass instead.' + ); + } } /** @@ -35,19 +93,23 @@ export class CollectionImpl implements ValueObject { * Note: `Seq.Keyed` is a conversion function and not a class, and does not * use the `new` keyword during construction. */ -export function Keyed( +export function KeyedCollection( collection?: Iterable<[K, V]> ): KeyedCollectionImpl; -export function Keyed(obj: { +export function KeyedCollection(obj: { [key: string]: V; }): KeyedCollectionImpl; -export function Keyed(value: unknown): KeyedCollectionImpl { +export function KeyedCollection( + value: unknown +): KeyedCollectionImpl { return isKeyed(value) ? value : KeyedSeq(value); } export class KeyedCollectionImpl extends CollectionImpl {} -export function IndexedCollection(value) { +export function IndexedCollection( + value: Iterable | ArrayLike +): IndexedCollectionImpl { return isIndexed(value) ? value : IndexedSeq(value); } @@ -74,12 +136,14 @@ export class IndexedCollectionImpl declare [Symbol.iterator]: () => IterableIterator; } -export function SetCollection(value) { +export function SetCollection( + value: Iterable | ArrayLike +): SetCollectionImpl { return isCollection(value) && !isAssociative(value) ? value : SetSeq(value); } export class SetCollectionImpl extends CollectionImpl {} -Collection.Keyed = Keyed; +Collection.Keyed = KeyedCollection; Collection.Indexed = IndexedCollection; Collection.Set = SetCollection; diff --git a/src/CollectionImpl.js b/src/CollectionImpl.js index d38323b6fe..0ddeda3f73 100644 --- a/src/CollectionImpl.js +++ b/src/CollectionImpl.js @@ -5,16 +5,9 @@ import { KeyedCollectionImpl, SetCollectionImpl, } from './Collection'; -import { hash } from './Hash'; -import { - ITERATE_ENTRIES, - ITERATE_KEYS, - ITERATE_VALUES, - Iterator, -} from './Iterator'; +import { ITERATE_KEYS, ITERATE_VALUES, Iterator } from './Iterator'; import { List } from './List'; import { Map } from './Map'; -import { imul, smi } from './Math'; import { concatFactory, countByFactory, @@ -65,10 +58,9 @@ import { toObject } from './methods/toObject'; import { IS_COLLECTION_SYMBOL } from './predicates/isCollection'; import { IS_INDEXED_SYMBOL, isIndexed } from './predicates/isIndexed'; import { IS_KEYED_SYMBOL, isKeyed } from './predicates/isKeyed'; -import { IS_ORDERED_SYMBOL, isOrdered } from './predicates/isOrdered'; +import { IS_ORDERED_SYMBOL } from './predicates/isOrdered'; import { toJS } from './toJS'; import assertNotInfinite from './utils/assertNotInfinite'; -import deepEqual from './utils/deepEqual'; import mixin from './utils/mixin'; import quoteString from './utils/quoteString'; @@ -176,22 +168,6 @@ mixin(CollectionImpl, { return this.some((value) => is(value, searchValue)); }, - entries() { - return this.__iterator(ITERATE_ENTRIES); - }, - - every(predicate, context) { - assertNotInfinite(this.size); - let returnValue = true; - this.__iterate((v, k, c) => { - if (!predicate.call(context, v, k, c)) { - returnValue = false; - return false; - } - }); - return returnValue; - }, - filter(predicate, context) { return reify(this, filterFactory(this, predicate, context, true)); }, @@ -301,9 +277,9 @@ mixin(CollectionImpl, { return countByFactory(this, grouper, context); }, - equals(other) { - return deepEqual(this, other); - }, + // equals(other) { + // return deepEqual(this, other); + // }, entrySeq() { // eslint-disable-next-line @typescript-eslint/no-this-alias @@ -482,9 +458,9 @@ mixin(CollectionImpl, { // ### Hashable Object - hashCode() { - return this.__hash || (this.__hash = hashCollection(this)); - }, + // hashCode() { + // return this.__hash || (this.__hash = hashCollection(this)); + // }, // ### Internal @@ -755,47 +731,3 @@ function defaultZipper(...values) { function defaultNegComparator(a, b) { return a < b ? 1 : a > b ? -1 : 0; } - -function hashCollection(collection) { - if (collection.size === Infinity) { - return 0; - } - const ordered = isOrdered(collection); - const keyed = isKeyed(collection); - let h = ordered ? 1 : 0; - - collection.__iterate( - keyed - ? ordered - ? (v, k) => { - h = (31 * h + hashMerge(hash(v), hash(k))) | 0; - } - : (v, k) => { - h = (h + hashMerge(hash(v), hash(k))) | 0; - } - : ordered - ? (v) => { - h = (31 * h + hash(v)) | 0; - } - : (v) => { - h = (h + hash(v)) | 0; - } - ); - - return murmurHashOfSize(collection.size, h); -} - -function murmurHashOfSize(size, h) { - h = imul(h, 0xcc9e2d51); - h = imul((h << 15) | (h >>> -15), 0x1b873593); - h = imul((h << 13) | (h >>> -13), 5); - h = ((h + 0xe6546b64) | 0) ^ size; - h = imul(h ^ (h >>> 16), 0x85ebca6b); - h = imul(h ^ (h >>> 13), 0xc2b2ae35); - h = smi(h ^ (h >>> 16)); - return h; -} - -function hashMerge(a, b) { - return (a ^ (b + 0x9e3779b9 + (a << 6) + (a >> 2))) | 0; // int -} diff --git a/src/Iterator.ts b/src/Iterator.ts index 953b56e799..63ad49b623 100644 --- a/src/Iterator.ts +++ b/src/Iterator.ts @@ -68,8 +68,6 @@ export function iteratorValue( const value = getValueFromType(type, k, v); // type === ITERATE_KEYS ? k : type === ITERATE_VALUES ? v : [k, v]; - // eslint-disable-next-line @typescript-eslint/no-unused-expressions -- TODO enable eslint here - if (iteratorResult) { iteratorResult.value = value; diff --git a/src/Range.ts b/src/Range.ts index fd33aedca8..a6c992e9db 100644 --- a/src/Range.ts +++ b/src/Range.ts @@ -115,7 +115,7 @@ export class RangeImpl extends IndexedSeqImpl implements Seq.Indexed { } override __iterate( - fn: (value: number, index: number, iter: this) => boolean, + fn: (value: number, index: number, iter: this) => boolean | void, reverse: boolean = false ): number { const size = this.size; @@ -149,7 +149,7 @@ export class RangeImpl extends IndexedSeqImpl implements Seq.Indexed { }); } - equals(other: unknown): boolean { + override equals(other: unknown): boolean { return other instanceof RangeImpl ? this._start === other._start && this._end === other._end && diff --git a/src/TrieUtils.ts b/src/TrieUtils.ts index 9a1c6f9580..46b22e839e 100644 --- a/src/TrieUtils.ts +++ b/src/TrieUtils.ts @@ -31,12 +31,10 @@ export function SetRef(ref: Ref): void { export function OwnerID() {} export function ensureSize(iter: CollectionImpl): number { - // @ts-expect-error size should exists on Collection if (iter.size === undefined) { - // @ts-expect-error size should exists on Collection, __iterate does exist on Collection iter.size = iter.__iterate(returnTrue); } - // @ts-expect-error size should exists on Collection + return iter.size; } diff --git a/src/predicates/isAssociative.ts b/src/predicates/isAssociative.ts index 5ee2da3963..834bf5f3c0 100644 --- a/src/predicates/isAssociative.ts +++ b/src/predicates/isAssociative.ts @@ -1,4 +1,4 @@ -import type { Collection } from '../../type-definitions/immutable'; +import type { IndexedCollectionImpl, KeyedCollectionImpl } from '../Collection'; import { isIndexed } from './isIndexed'; import { isKeyed } from './isKeyed'; @@ -19,7 +19,7 @@ import { isKeyed } from './isKeyed'; export function isAssociative( maybeAssociative: unknown ): maybeAssociative is - | Collection.Keyed - | Collection.Indexed { + | KeyedCollectionImpl + | IndexedCollectionImpl { return isKeyed(maybeAssociative) || isIndexed(maybeAssociative); } diff --git a/src/predicates/isCollection.ts b/src/predicates/isCollection.ts index 738a4614e0..607bdf4ab2 100644 --- a/src/predicates/isCollection.ts +++ b/src/predicates/isCollection.ts @@ -1,4 +1,4 @@ -import type { Collection } from '../../type-definitions/immutable'; +import type { CollectionImpl } from '../Collection'; // Note: value is unchanged to not break immutable-devtools. export const IS_COLLECTION_SYMBOL = '@@__IMMUTABLE_ITERABLE__@@'; @@ -18,7 +18,7 @@ export const IS_COLLECTION_SYMBOL = '@@__IMMUTABLE_ITERABLE__@@'; */ export function isCollection( maybeCollection: unknown -): maybeCollection is Collection { +): maybeCollection is CollectionImpl { return Boolean( maybeCollection && // @ts-expect-error: maybeCollection is typed as `{}`, need to change in 6.0 to `maybeCollection && typeof maybeCollection === 'object' && IS_COLLECTION_SYMBOL in maybeCollection` diff --git a/src/predicates/isIndexed.ts b/src/predicates/isIndexed.ts index 3e20595af5..f6d76ea449 100644 --- a/src/predicates/isIndexed.ts +++ b/src/predicates/isIndexed.ts @@ -1,4 +1,4 @@ -import type { Collection } from '../../type-definitions/immutable'; +import type { IndexedCollectionImpl } from '../Collection'; export const IS_INDEXED_SYMBOL = '@@__IMMUTABLE_INDEXED__@@'; @@ -18,7 +18,7 @@ export const IS_INDEXED_SYMBOL = '@@__IMMUTABLE_INDEXED__@@'; */ export function isIndexed( maybeIndexed: unknown -): maybeIndexed is Collection.Indexed { +): maybeIndexed is IndexedCollectionImpl { return Boolean( maybeIndexed && // @ts-expect-error: maybeIndexed is typed as `{}`, need to change in 6.0 to `maybeIndexed && typeof maybeIndexed === 'object' && IS_INDEXED_SYMBOL in maybeIndexed` diff --git a/src/predicates/isKeyed.ts b/src/predicates/isKeyed.ts index 35f7b7e9c8..0b71e66a9f 100644 --- a/src/predicates/isKeyed.ts +++ b/src/predicates/isKeyed.ts @@ -1,4 +1,4 @@ -import type { Collection } from '../../type-definitions/immutable'; +import type { KeyedCollectionImpl } from '../Collection'; export const IS_KEYED_SYMBOL = '@@__IMMUTABLE_KEYED__@@'; @@ -17,7 +17,7 @@ export const IS_KEYED_SYMBOL = '@@__IMMUTABLE_KEYED__@@'; */ export function isKeyed( maybeKeyed: unknown -): maybeKeyed is Collection.Keyed { +): maybeKeyed is KeyedCollectionImpl { return Boolean( maybeKeyed && // @ts-expect-error: maybeKeyed is typed as `{}`, need to change in 6.0 to `maybeKeyed && typeof maybeKeyed === 'object' && IS_KEYED_SYMBOL in maybeKeyed` diff --git a/src/predicates/isOrdered.ts b/src/predicates/isOrdered.ts index 2e20d415ff..b0ff54c9bf 100644 --- a/src/predicates/isOrdered.ts +++ b/src/predicates/isOrdered.ts @@ -20,6 +20,9 @@ export const IS_ORDERED_SYMBOL = '@@__IMMUTABLE_ORDERED__@@'; export function isOrdered( maybeOrdered: Iterable ): maybeOrdered is OrderedCollection; +export function isOrdered( + maybeOrdered: unknown +): maybeOrdered is OrderedCollection; export function isOrdered( maybeOrdered: unknown ): maybeOrdered is OrderedCollection { diff --git a/src/toJS.ts b/src/toJS.ts index 38370d3727..4aa220e4a5 100644 --- a/src/toJS.ts +++ b/src/toJS.ts @@ -23,9 +23,8 @@ export function toJS( } if (isKeyed(value)) { const result: { [key: string]: unknown } = {}; - // @ts-expect-error `__iterate` exists on all Keyed collections but method is not defined in the type value.__iterate((v, k) => { - result[k] = toJS(v); + result[String(k)] = toJS(v); }); return result; } diff --git a/src/utils/deepEqual.ts b/src/utils/deepEqual.ts index f092abf0a6..61568e38fc 100644 --- a/src/utils/deepEqual.ts +++ b/src/utils/deepEqual.ts @@ -1,4 +1,4 @@ -import type { Collection } from '../../type-definitions/immutable'; +import type { CollectionImpl } from '../Collection'; import type { RangeImpl as Range } from '../Range'; import type { RepeatImpl as Repeat } from '../Repeat'; import { NOT_SET } from '../TrieUtils'; @@ -10,7 +10,7 @@ import { isKeyed } from '../predicates/isKeyed'; import { isOrdered } from '../predicates/isOrdered'; export default function deepEqual( - a: Range | Repeat | Collection, + a: Range | Repeat | CollectionImpl, b: unknown ): boolean { if (a === b) { @@ -19,7 +19,6 @@ export default function deepEqual( if ( !isCollection(b) || - // @ts-expect-error size should exists on Collection (a.size !== undefined && b.size !== undefined && a.size !== b.size) || // @ts-expect-error __hash exists on Collection (a.__hash !== undefined && @@ -29,24 +28,20 @@ export default function deepEqual( a.__hash !== b.__hash) || isKeyed(a) !== isKeyed(b) || isIndexed(a) !== isIndexed(b) || - // @ts-expect-error Range extends Collection, which implements [Symbol.iterator], so it is valid isOrdered(a) !== isOrdered(b) ) { return false; } - // @ts-expect-error size should exists on Collection if (a.size === 0 && b.size === 0) { return true; } const notAssociative = !isAssociative(a); - // @ts-expect-error Range extends Collection, which implements [Symbol.iterator], so it is valid if (isOrdered(a)) { const entries = a.entries(); - // @ts-expect-error need to cast as boolean - return ( + return !!( b.every((v, k) => { const entry = entries.next().value; return entry && is(entry[1], v) && (notAssociative || is(entry[0], k)); @@ -57,9 +52,10 @@ export default function deepEqual( let flipped = false; if (a.size === undefined) { - // @ts-expect-error size should exists on Collection if (b.size === undefined) { + // @ts-expect-error cacheResult might be implemented on some collections if (typeof a.cacheResult === 'function') { + // @ts-expect-error cacheResult might be implemented on some collections a.cacheResult(); } } else { @@ -89,9 +85,5 @@ export default function deepEqual( } }); - return ( - allEqual && - // @ts-expect-error size should exists on Collection - a.size === bSize - ); + return allEqual && a.size === bSize; } diff --git a/src/utils/hashCollection.ts b/src/utils/hashCollection.ts new file mode 100644 index 0000000000..e0b5b22f83 --- /dev/null +++ b/src/utils/hashCollection.ts @@ -0,0 +1,49 @@ +import type { CollectionImpl } from '../Collection'; +import { hash } from '../Hash'; +import { imul, smi } from '../Math'; +import { isKeyed } from '../predicates/isKeyed'; +import { isOrdered } from '../predicates/isOrdered'; + +export function hashCollection(collection: CollectionImpl): number { + if (collection.size === Infinity) { + return 0; + } + const ordered = isOrdered(collection); + const keyed = isKeyed(collection); + let h: number = ordered ? 1 : 0; + + collection.__iterate( + keyed + ? ordered + ? (v: V, k: K): void => { + h = (31 * h + hashMerge(hash(v), hash(k))) | 0; + } + : (v: V, k: K): void => { + h = (h + hashMerge(hash(v), hash(k))) | 0; + } + : ordered + ? (v: V): void => { + h = (31 * h + hash(v)) | 0; + } + : (v: V): void => { + h = (h + hash(v)) | 0; + } + ); + + return murmurHashOfSize(collection.size, h); +} + +function murmurHashOfSize(size: number, h: number): number { + h = imul(h, 0xcc9e2d51); + h = imul((h << 15) | (h >>> -15), 0x1b873593); + h = imul((h << 13) | (h >>> -13), 5); + h = ((h + 0xe6546b64) | 0) ^ size; + h = imul(h ^ (h >>> 16), 0x85ebca6b); + h = imul(h ^ (h >>> 13), 0xc2b2ae35); + h = smi(h ^ (h >>> 16)); + return h; +} + +function hashMerge(a: number, b: number): number { + return (a ^ (b + 0x9e3779b9 + (a << 6) + (a >> 2))) | 0; // int +} From 304a9044cf3a8d1fad60073a00fb060e44f7a9ec Mon Sep 17 00:00:00 2001 From: Julien Deniau Date: Sat, 28 Jun 2025 22:18:41 +0000 Subject: [PATCH 4/4] Use CollectionImpl instead of Collection from .d.ts file --- src/functional/get.ts | 17 +++++++++++------ src/functional/remove.ts | 9 +++++---- src/functional/set.ts | 15 ++++++++------- src/functional/update.ts | 7 ++++--- src/functional/updateIn.ts | 12 ++++++------ src/predicates/isImmutable.ts | 5 +++-- src/predicates/isValueObject.ts | 2 +- src/utils/isDataStructure.ts | 5 +++-- 8 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/functional/get.ts b/src/functional/get.ts index 9ec97d8070..1b28ac0264 100644 --- a/src/functional/get.ts +++ b/src/functional/get.ts @@ -1,4 +1,5 @@ -import type { Collection, Record } from '../../type-definitions/immutable'; +import type { Record } from '../../type-definitions/immutable'; +import type { CollectionImpl } from '../Collection'; import { isImmutable } from '../predicates/isImmutable'; import { has } from './has'; @@ -9,9 +10,12 @@ import { has } from './has'; * A functional alternative to `collection.get(key)` which will also work on * plain Objects and Arrays as an alternative for `collection[key]`. */ -export function get(collection: Collection, key: K): V | undefined; +export function get( + collection: CollectionImpl, + key: K +): V | undefined; export function get( - collection: Collection, + collection: CollectionImpl, key: K, notSetValue: NSV ): V | NSV; @@ -41,17 +45,18 @@ export function get( notSetValue: NSV ): V | NSV; export function get( - collection: Collection | Array | { [key: string]: V }, + collection: CollectionImpl | Array | { [key: string]: V }, key: K, notSetValue?: NSV ): V | NSV; export function get( - collection: Collection | Array | { [key: string]: V }, + collection: CollectionImpl | Array | { [key: string]: V }, key: K, notSetValue?: NSV ): V | NSV { return isImmutable(collection) - ? collection.get(key, notSetValue) + ? // @ts-expect-error "get" is still in the mixin for now + collection.get(key, notSetValue) : !has(collection, key) ? notSetValue : // @ts-expect-error weird "get" here, diff --git a/src/functional/remove.ts b/src/functional/remove.ts index 970384dce6..d2155ee522 100644 --- a/src/functional/remove.ts +++ b/src/functional/remove.ts @@ -1,4 +1,5 @@ -import type { Collection, Record } from '../../type-definitions/immutable'; +import type { Record } from '../../type-definitions/immutable'; +import type { CollectionImpl } from '../Collection'; import { isImmutable } from '../predicates/isImmutable'; import hasOwnProperty from '../utils/hasOwnProperty'; import isDataStructure from '../utils/isDataStructure'; @@ -11,7 +12,7 @@ import shallowCopy from '../utils/shallowCopy'; * with plain Objects and Arrays as an alternative for * `delete collectionCopy[key]`. */ -export function remove>( +export function remove>( collection: C, key: K ): C; @@ -29,13 +30,13 @@ export function remove< export function remove< K, C extends - | Collection + | CollectionImpl | Array | { [key: PropertyKey]: unknown }, >(collection: C, key: K): C; export function remove( collection: - | Collection + | CollectionImpl | Array | { [key: PropertyKey]: unknown }, key: K diff --git a/src/functional/set.ts b/src/functional/set.ts index 2f1d41dd5b..6de55df96b 100644 --- a/src/functional/set.ts +++ b/src/functional/set.ts @@ -1,4 +1,5 @@ -import type { Collection, Record } from '../../type-definitions/immutable'; +import type { Record } from '../../type-definitions/immutable'; +import type { CollectionImpl } from '../Collection'; import { isImmutable } from '../predicates/isImmutable'; import hasOwnProperty from '../utils/hasOwnProperty'; import isDataStructure from '../utils/isDataStructure'; @@ -12,7 +13,7 @@ import shallowCopy from '../utils/shallowCopy'; * work with plain Objects and Arrays as an alternative for * `collectionCopy[key] = value`. */ -export function set>( +export function set>( collection: C, key: K, value: V @@ -33,11 +34,11 @@ export function set( key: string, value: V ): C; -export function set | { [key: string]: V }>( - collection: C, - key: K | string, - value: V -): C { +export function set< + K, + V, + C extends CollectionImpl | { [key: string]: V }, +>(collection: C, key: K | string, value: V): C { if (!isDataStructure(collection)) { throw new TypeError( 'Cannot update non-data-structure value: ' + collection diff --git a/src/functional/update.ts b/src/functional/update.ts index 07dbd8e055..ab71026755 100644 --- a/src/functional/update.ts +++ b/src/functional/update.ts @@ -1,4 +1,5 @@ -import type { Collection, Record } from '../../type-definitions/immutable'; +import type { Record } from '../../type-definitions/immutable'; +import type { CollectionImpl } from '../Collection'; import { type PossibleCollection, updateIn } from './updateIn'; type UpdaterFunction = (value: V | undefined) => V | undefined; @@ -12,12 +13,12 @@ type UpdaterFunctionWithNSV = (value: V | NSV) => V; * work with plain Objects and Arrays as an alternative for * `collectionCopy[key] = fn(collection[key])`. */ -export function update>( +export function update>( collection: C, key: K, updater: (value: V | undefined) => V | undefined ): C; -export function update, NSV>( +export function update, NSV>( collection: C, key: K, notSetValue: NSV, diff --git a/src/functional/updateIn.ts b/src/functional/updateIn.ts index 4c0120331c..10e12c5eb8 100644 --- a/src/functional/updateIn.ts +++ b/src/functional/updateIn.ts @@ -1,9 +1,10 @@ import type { - Collection, KeyPath, Record, RetrievePath, + Collection, } from '../../type-definitions/immutable'; +import type { CollectionImpl } from '../Collection'; import { emptyMap } from '../Map'; import { NOT_SET } from '../TrieUtils'; import { isImmutable } from '../predicates/isImmutable'; @@ -23,9 +24,8 @@ import { set } from './set'; */ export type PossibleCollection = - | Collection - | Record - | Array; + // TODO migrate to CollectionImpl in the end + Collection | Record | Array; type UpdaterFunction = ( value: RetrievePath> | undefined @@ -34,12 +34,12 @@ type UpdaterFunctionWithNSV = ( value: RetrievePath> | NSV ) => unknown; -export function updateIn>( +export function updateIn>( collection: C, keyPath: KeyPath, updater: UpdaterFunction ): C; -export function updateIn, NSV>( +export function updateIn, NSV>( collection: C, keyPath: KeyPath, notSetValue: NSV, diff --git a/src/predicates/isImmutable.ts b/src/predicates/isImmutable.ts index d01bd03afb..e9798d52ba 100644 --- a/src/predicates/isImmutable.ts +++ b/src/predicates/isImmutable.ts @@ -1,4 +1,5 @@ -import type { Collection, Record } from '../../type-definitions/immutable'; +import type { Record } from '../../type-definitions/immutable'; +import type { CollectionImpl } from '../Collection'; import { isCollection } from './isCollection'; import { isRecord } from './isRecord'; @@ -19,6 +20,6 @@ import { isRecord } from './isRecord'; */ export function isImmutable( maybeImmutable: unknown -): maybeImmutable is Collection | Record { +): maybeImmutable is CollectionImpl | Record { return isCollection(maybeImmutable) || isRecord(maybeImmutable); } diff --git a/src/predicates/isValueObject.ts b/src/predicates/isValueObject.ts index f603b517ea..8798aeacf9 100644 --- a/src/predicates/isValueObject.ts +++ b/src/predicates/isValueObject.ts @@ -1,4 +1,4 @@ -import type { ValueObject } from '../../type-definitions/immutable'; +import type ValueObject from '../ValueObject'; /** * True if `maybeValue` is a JavaScript Object which has *both* `equals()` diff --git a/src/utils/isDataStructure.ts b/src/utils/isDataStructure.ts index e71c55bb7d..9637c19294 100644 --- a/src/utils/isDataStructure.ts +++ b/src/utils/isDataStructure.ts @@ -1,4 +1,5 @@ -import type { Collection, Record } from '../../type-definitions/immutable'; +import type { Record } from '../../type-definitions/immutable'; +import type { CollectionImpl } from '../Collection'; import { isImmutable } from '../predicates/isImmutable'; import isPlainObj from './isPlainObj'; @@ -9,7 +10,7 @@ import isPlainObj from './isPlainObj'; export default function isDataStructure( value: unknown ): value is - | Collection + | CollectionImpl | Record | Array | object {