From b7e0fe36895842a564567ccde4513aa3c6b71ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Sun, 26 Jan 2025 13:14:48 +0000 Subject: [PATCH] feat(collection): honour subclassing via `@@species` in static methods (#10723) * feat(collection): use @@species in static methods * test(collection): subclassing tests * chore: trigger ci --------- Co-authored-by: almeidx --- .../collection/__tests__/collection.test.ts | 55 +++++++++++++++++++ packages/collection/src/collection.ts | 11 +++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/packages/collection/__tests__/collection.test.ts b/packages/collection/__tests__/collection.test.ts index 81ccd802f..eaff8418b 100644 --- a/packages/collection/__tests__/collection.test.ts +++ b/packages/collection/__tests__/collection.test.ts @@ -1099,3 +1099,58 @@ describe('findLastKey() tests', () => { }, null); }); }); + +describe('subclassing tests', () => { + class DerivedCollection extends Collection {} + + test('constructor[Symbol.species]', () => { + expect(DerivedCollection[Symbol.species]).toStrictEqual(DerivedCollection); + }); + + describe('methods that construct new collections return subclassed objects', () => { + const coll = new DerivedCollection(); + + test('filter()', () => { + expect(coll.filter(Boolean)).toBeInstanceOf(DerivedCollection); + }); + test('partition()', () => { + for (const partition of coll.partition(Boolean)) { + expect(partition).toBeInstanceOf(DerivedCollection); + } + }); + test('flatMap()', () => { + expect(coll.flatMap(() => new Collection())).toBeInstanceOf(DerivedCollection); + }); + test('mapValues()', () => { + expect(coll.mapValues(Object)).toBeInstanceOf(DerivedCollection); + }); + test('clone()', () => { + expect(coll.clone()).toBeInstanceOf(DerivedCollection); + }); + test('intersection()', () => { + expect(coll.intersection(new Collection())).toBeInstanceOf(DerivedCollection); + }); + test('union()', () => { + expect(coll.union(new Collection())).toBeInstanceOf(DerivedCollection); + }); + test('difference()', () => { + expect(coll.difference(new Collection())).toBeInstanceOf(DerivedCollection); + }); + test('symmetricDifference()', () => { + expect(coll.symmetricDifference(new Collection())).toBeInstanceOf(DerivedCollection); + }); + test('merge()', () => { + const fn = () => ({ keep: false }) as const; // eslint-disable-line unicorn/consistent-function-scoping + expect(coll.merge(new Collection(), fn, fn, fn)).toBeInstanceOf(DerivedCollection); + }); + test('toReversed()', () => { + expect(coll.toReversed()).toBeInstanceOf(DerivedCollection); + }); + test('toSorted()', () => { + expect(coll.toSorted()).toBeInstanceOf(DerivedCollection); + }); + test('Collection.combineEntries()', () => { + expect(DerivedCollection.combineEntries([], Object)).toBeInstanceOf(DerivedCollection); + }); + }); +}); diff --git a/packages/collection/src/collection.ts b/packages/collection/src/collection.ts index 288b2291c..944df07a6 100644 --- a/packages/collection/src/collection.ts +++ b/packages/collection/src/collection.ts @@ -11,11 +11,11 @@ export type ReadonlyCollection = Omit< export interface Collection { /** - * Ambient declaration to allow `this.constructor[@@species]` in class methods. + * Ambient declaration to allow references to `this.constructor` in class methods. * * @internal */ - constructor: typeof Collection & { readonly [Symbol.species]: typeof Collection }; + constructor: typeof Collection; } /** @@ -1076,7 +1076,7 @@ export class Collection extends Map { entries: Iterable<[Key, Value]>, combine: (firstValue: Value, secondValue: Value, key: Key) => Value, ): Collection { - const coll = new Collection(); + const coll = new this[Symbol.species](); for (const [key, value] of entries) { if (coll.has(key)) { coll.set(key, combine(coll.get(key)!, value, key)); @@ -1087,6 +1087,11 @@ export class Collection extends Map { return coll; } + + /** + * @internal + */ + declare public static readonly [Symbol.species]: typeof Collection; } /**