skip to Main Content

I’m using TypeScript and I want to extend Array to enforce a specific argument. This way, I can make sure that whenever I create a FruitArray, the first element is a fruit.

So I tried extending Array in a class:

class FruitArray extends Array {
  constructor(fruit: FruitsEnum, ...args: string[]) {
    if (!enumContains(FruitsEnum, fruit)) {
      throw new Error('Must initialize with a fruit!');
    }
    super(fruit, ...args);
  }
}

However, the linter complains:

A spread argument must either have a tuple type or be passed to a rest
parameter.

I think I am handling super() wrong, but I don’t understand how to get it to work.

2

Answers


  1. Why do you need to extend Array? can’t you use something like this?

    const fruitArray = new Array<FruitsEnum>()
    

    for the class implementation:

    Why does fruits array have 2 types? string and FruitsEnum? Why dont you just use FruitsEnum If you are going to enforce FruitsEnum type?

    enum FruitsEnum {
    
    }
    
    class FruitArray extends Array<FruitsEnum> {
      constructor(fruit: FruitsEnum, ...args: FruitsEnum[]) {
        ...
        super(fruit, ...args);
        ...
      }
    }
    
    Login or Signup to reply.
  2. First I’ll address the problem you asked about, but then point out a couple of things that may bite you with the approach of extending Array.

    Re the specific error, it looks like the problem is that you’ve extended Array without specifying its element type. With the error (playground):

    class FruitArray extends Array {
        //                        ^−−−−−− no type parameter here
        // ...
    }
    

    Specifying an element type makes the error go away, for example here I’m giving FruitsEnum | string (playground):

    class FruitArray extends Array<FruitsEnum | string> {
        // ...
    }
    

    Separately, though, a couple of issues with extending Array like this:

    1. Your code isn’t handling all of the construction signatures it has to handle to properly subclass Array. It needs to support at least these:

      • A single argument that’s a number = create an empty array with the given length
      • A single argument that isn’t a number = create an array with that one value as an element
      • Multiple arguments = create an array with those elements

      (I don’t think it needs to support zero arguments. In the JavaScript specification, when creating arrays it does so via ArrayCreate and ArraySpeciesCreate, both of which expect a length argument. But double-check me before relying on that.)

      TypeScript doesn’t complain about not handling that first argument being a number (because overlaying types on JavaScript’s Array after the fact is hard), but your fruit parameter’s value may well be a number. Example on the playground:

      const f = new FruitArray(FruitsEnum.APPLE, "x");
      const f2 = f.slice(1); // <== Calls your constructor with 1 as the only argument
      
    2. While you can ensure that FruitArray starts out with a FruitsEnum value as its first element, that can soon become untrue (playground):

      const f = new FruitArray(FruitsEnum.APPLE, "x");
      f[0] = "x";
      console.log(JSON.stringify(f));
      //                         ^? const f: FruitArray
      

      There, f is still a FruitArray, but the first element is now a string, not a FruitsEnum.

    You may be better off defining a tuple type or your own class that doesn’t directly extend Array (it could use an array in its implementation), but it depends on what you’re going to do with it.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search