2

I would like to create an Array-Like class and add some properties to it. So far I have this code:

class MyArray<T> extends Array<T> {
    constructor(items: T[], data?: any) {
        super(...items)

        this.#data = data
    }

    #data: any

    public get data() {
        return this.#data
    }
}

But this code fails with error "Spread syntax requires ...iterable[Symbol.iterator] to be a function":

const myArray = new MyArray([1, 2, 3], {})
myArray.map(item => item+1)

I guess there something wrong with my constructor. Could anyone point me in the right direction please? See TS playground

8
  • This might be a problem in the playground. Note that removing the console.log also removes the error. This means it's not coming from invoking the constructor. Also ` console.log(myArray[0], myArray[1], myArray[2], myArray.length, myArray.data);` does work correctly. Only console.log(myArray) leads to an error: tsplay.dev/WJ3Gvw Commented Mar 22, 2024 at 10:24
  • 1
    You're right. I only tried removing the console.log but I think the issue is that it tries to initialise the clone of an array how a normal array works. If you call .map(x => x) it calls the constructor with a single argument 3 which is the length of the array that is being cloned. This is in line with how the vanilla Array constructor works - passing in a single number will initialise an array with this many empty slots. It's probably how cloning works (I'll have to verify but seems to be the case). Which means that the child constructor simply doesn't work the same way. Commented Mar 22, 2024 at 11:16
  • 1
    Hmm, it's indeed the cloning that's messing it up. When .map() is called (and other methods like .slice()) JS would grab the constructor of the array (via @@species if it needs to be overridden), creates a new array effectively by new Ctor(originalArray.length) and then (for .map() at least) it goes over and fills the slots based on the mapping function. You can sort of change the constructor to be more correct for this operation tsplay.dev/w2gZbN However, you lose any additional properties with no easy way to transfer them. Extending seems a bad approach here. Commented Mar 22, 2024 at 11:46
  • 1
    @jcalz "It's ultimately JS being insane" lol. I can find the method in the madness there. You want to be able to do .map() and get the same object back and what JS has decided to do to ensure it works. Unfortunately the way it's done also means that extending arrays is severely limited. You can very easily add new methods, or override existing ones. But anything other than that, like adding more properties is essentially a no-go as far as I can see. The only solutions are various forms of hacks. Which is a shame. Commented Mar 22, 2024 at 12:53
  • 1
    I understand how JS is doing it, but for a type system to catch it, it would have to say "okay, you can subclass Array, but you pretty much must make the construct signature 100% compatible with whatever the @@species happens to be." TS doesn't have a way to constrain construct signatures when you write class X extends Y {} so it's pretty much impossible to express. JS is often actively hostile to writing a sound type system and so TS has to make weird tradeoffs. Maybe I should say "TS is insane for trying to represent JS as having strong types." Commented Mar 22, 2024 at 12:57

2 Answers 2

-1

The error you're encountering is because Array constructor expects iterable arguments, and MyArray does not handle the spread of items correctly in the constructor. Here's a corrected version of your code:

class MyArray<T> extends Array<T> {
    constructor(items: T[], data?: any) {
        // If data is provided, spread items inside super call; otherwise, pass empty array
        super(data ? ...items : undefined);

        this.#data = data;
    }

    #data: any;

    public get data() {
        return this.#data;
    }
}

Explanation:

In the constructor, the spread operator ... is used to spread the items array if data is provided. If data is not provided, an empty array is passed to the super call. This ensures that the super call receives an iterable argument as expected. With this corrected code, your example should work as expected:

const myArray = new MyArray([1, 2, 3], {});
console.log(myArray); // Output: MyArray [ 1, 2, 3 ]
console.log(myArray.data); // Output: {}
Sign up to request clarification or add additional context in comments.

2 Comments

...items would always be a valid spread, It might result in different behaviour with the array constructor because new Array(3) and new Array(3, 2, 1) do different things (former creates an array with 3 empty slots, the latter - an array with three elements). However, the code seems correct. "Array constructor expects iterable arguments" is plain wrong - passing an iterable into an array constructor would just use it as an item to put in the array. The constructor expects either nothing, or a single number for empty slots, or anything else is items for the array.
@Het Modi I am unable to compile the code you suggested: Argument of type 'undefined' is not assignable to parameter of type 'T'. 'T' could be instantiated with an arbitrary type which could be unrelated to 'undefined'. Expression expected. ',' expected.
-1

Let's revise the code to handle the case where data is not provided:

class MyArray<T> extends Array<T> {
    #data: any;

    constructor(items: T[], data?: any) {
        // If data is provided, pass items to super(); otherwise, pass no arguments
        super(...(data ? items : []));

        this.#data = data;
    }

    public get data() {
        return this.#data;
    }
}

In this revised version:

If data is provided, items are spread inside the super() call. If data is not provided, an empty array [] is passed to super().

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.