Home > Mobile >  Overriding Array toString in TypeScript
Overriding Array toString in TypeScript

Time:07-20

I want to override the toString without explicitly instantiating an object with new.

See following example:

class SemicolonArray extends Array {
  public toString() {
    return this.join(';');
  }
}

class Foo {
  constructor(arr: SemicolonArray) {
    this.arr = arr;
  }
  arr: SemicolonArray;
}

const foo = new Foo([1,2,3]);
console.log(foo.arr.toString());  // "1,2,3"

I would however expect "1;2;3". Is there a way to do this in TypeScript?

Edit:

To clarify further, I would like to do this:

interface Bar {
  hello :string

  bar() : string
}

class Foo implements Bar {
  hello: string
  bar() {
    return this.hello;
  }
}

const foo = { hello: 'hello' } as Foo
foo.bar()

However bar is not a method. Is there a way to make sure that every object casted as Foo has the bar method?

CodePudding user response:

There are two distinct issues here:

  1. Making array literal syntax create a SemicolonArray.

  2. Foo is expecting a SemicolonArray but TypeScript doesn't complain when you pass in a base array instead, since the shape of a base array matches the shape of a SemicolonArry.

Making array literal create subclass

There's no way to make an array literal create an instance of an array subclass. Literal notation cannot be overridden. An array literal can only create a standard array.

You could use SemicolonArray.from:

const foo = new Foo(SemicolonArray.from([1,2,3]));

...but new SemicolonArray(1, 2, 3) is probably simpler.

Or you could have Foo convert a base array to a SemicolonArray:

class Foo {
    constructor(arr: SemicolonArray) {
        this.arr = arr instanceof SemicolonArray ? arr : SemicolonArray.from(arr);
    }
    arr: SemicolonArray;
}
// ...
const foo = new Foo([1, 2, 3]);

No error when passing base array

As you've discovered, with your current definition of SemicolonArray, TypeScript won't complain about a base array being used instead, since SemicolonArray doesn't define any non-method properties. That's because TypeScript's type system is structural (based on the shape of types; what properties they have and their types), not nominal (based on the name [identity] of types).

That's likely to lead to errors. You can solve it by adding a branding property to SemicolonArray:

class SemicolonArray extends Array {
    readonly __type__ = "SemicolonArray"; // ***
    public toString() {
        return this.join(";");
    }
}

Now, TypeScript won't allow a base array where a SemicolonArray is expected.


As always with array subclasses, beware that the Array constructor handles its arguments very idiosyncratically. Your subclass is fine because it doesn't define its own constructor, but if you end up adding one, be sure to handle the complexity.


Re your edit:

However bar is not a method. Is there a way to make sure that every object casted as Foo has the bar method?

No. In fact, TypeScript doesn't have casting at all. It has type assertions. People sometimes use the term "cast" but it's inaccurate. In a language with casting, a cast can actually transform a value (it doesn't necessarily, but it can). For instance, consider this C code:

double a = 2.4;
int b = (int)a;
printf("%d\n", b); // 2

The cast operation (int) actually transforms the value: the operand is a, which is of type double, but the result of the cast operation is an int. The bit pattern of the value is actually transformed at runtime from a double bit pattern to an int bit pattern, using a conversion process.

Type assertions don't do that. They have no runtime aspect at all, they completely disappear. All that a type assertion does is tell TypeScript that you're sure that the operand is actually a valid instance of the type. The value is still what it was, the only thing that's changed is TypeScript's understanding of what type it is.

If you want to convert something to something else, a type assertion doesn't do it. You have to use a conversion of some kind (like SemicolonArray.from) that actually produces a new thing of the correct type.

(For the avoidance of doubt: JavaScript has a fair bit of built-in conversion, such as when you do "42" / 2 or "x" 2. But TypeScript doesn't, other than the things it inherits from JavaScript [like those conversions].)

  • Related