Skip to content

Commit e9e235b

Browse files
authored
Merge pull request #1 from jaysuk/fix/model-collection-species
Fix ModelCollection missing Symbol.species, crashing on .push() after filter/map/slice
2 parents f4286eb + 4a5969e commit e9e235b

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

__tests__/collection.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ModelCollection } from "../src";
2+
import type { IModelObject } from "../src";
3+
4+
class Item implements IModelObject {
5+
value: number = 0;
6+
update(jsonElement: any): IModelObject | null {
7+
this.value = jsonElement?.value ?? 0;
8+
return this;
9+
}
10+
}
11+
12+
test("array methods derived from a ModelCollection return plain arrays", () => {
13+
const collection = new ModelCollection(Item);
14+
collection.push(new Item().update({ value: 1 }) as Item, new Item().update({ value: 2 }) as Item);
15+
16+
const filtered = collection.filter(() => true);
17+
expect(filtered).not.toBeInstanceOf(ModelCollection);
18+
19+
const mapped = collection.map((item) => item);
20+
expect(mapped).not.toBeInstanceOf(ModelCollection);
21+
22+
// Regression: before overriding Symbol.species, `filtered`/`mapped` were still ModelCollection
23+
// instances whose $itemConstructor had been corrupted to a number (the array length) by the
24+
// default ES2015 species-construction protocol (`new ModelCollection(length)`). Pushing onto
25+
// them then threw "Right-hand side of 'instanceof' is not an object" instead of behaving like
26+
// a normal array.
27+
expect(() => filtered.push(new Item().update({ value: 3 }) as Item)).not.toThrow();
28+
expect(filtered.length).toBe(3);
29+
});

src/ModelCollection.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,22 @@ function createItem<T>(collection: IModelCollection<T>, index: number): T {
3030
* Class for storing model object items in an array
3131
*/
3232
export class ModelCollection<T extends IModelObject | null> extends Array<T> implements IModelObject {
33+
/**
34+
* Without this, Array methods that derive a new array from this one (filter, map, slice, concat,
35+
* etc.) build the result via the ES2015 species-construction protocol, which for an Array
36+
* subclass means calling `new ModelCollection(length)` -- a single numeric argument, matching
37+
* the plain Array(length) constructor signature. This class's own constructor instead treats
38+
* that first argument as `itemConstructor`, so the derived array ends up with $itemConstructor
39+
* set to a number. Any later `.push()` on it then throws "Right-hand side of 'instanceof' is not
40+
* an object", since push() checks `item instanceof that.$itemConstructor`. Overriding the species
41+
* to plain Array sidesteps this entirely: derived arrays are ordinary Arrays, and only genuine
42+
* ModelCollection instances (constructed directly with an item type) get the custom push/update
43+
* behaviour.
44+
*/
45+
static override get [Symbol.species](): ArrayConstructor {
46+
return Array;
47+
}
48+
3349
/**
3450
* Constructor of this class
3551
* @param itemConstructor Item constructor type that items must derive from

0 commit comments

Comments
 (0)