yqlim
yqlim

Reputation: 7098

TypeScript: Extending a native class gives incorrect prototype

Question

In TypeScript, when I do:

class B { ... }
class A extends B { ... }

I can call methods/getters/setters of prototype A normally, without error.

However, when I do the same thing with native class:

class A extends Array { ... }

When I call methods/getters/setters of prototype A, it throws because properties in prototype A are all undefined.

Why?

Examples

Given this working code:

class Animal {

  protected name: string;
  protected age: number;

  constructor(name: string, age: number){
    this.name = name;
  }

  eat(){
    console.log(`${this.name} has eaten.`);
  }

  sleep(){
    console.log(`${this.name} has slept.`);
  }

}

class Dog extends Animal {

  constructor(...args: [string, number]){
    super(...args);
  }

  bark(){
    console.log(`${this.name} has barked.`);
  }

}

const dog = new Dog('doggo', 2);
dog.eat();
dog.bark();

It will be transpiled into:

var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var Animal = /** @class */ (function () {
    function Animal(name, age) {
        this.name = name;
    }
    Animal.prototype.eat = function () {
        console.log(this.name + " has eaten.");
    };
    Animal.prototype.sleep = function () {
        console.log(this.name + " has slept.");
    };
    return Animal;
}());
var Dog = /** @class */ (function (_super) {
    __extends(Dog, _super);
    function Dog() {
        var args = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            args[_i] = arguments[_i];
        }
        return _super.apply(this, args) || this;
    }
    Dog.prototype.bark = function () {
        console.log(this.name + " has barked.");
    };
    return Dog;
}(Animal));
var dog = new Dog('doggo', 2);
dog.eat();
dog.bark();

So far, so good.

Now when I try to extend a native class (in this case, Array):

class List<T> extends Array<T> {

  private index: number;

  constructor(...items: T[]) {
    super(...items);
    Object.defineProperty(this, 'index', {
      value: 0,
      writable: true,
      configurable: true
    });
  }

  get current() {
    return this[this.index];
  }

  get max() {
    return this.length - 1;
  }

  next() {
    return this.index === this.max
      ? this.first()
      : this[++this.index];
  }

  prev() {
    return this.index === 0
      ? this.last()
      : this[--this.index];
  }

  first() {
    return this[this.index = 0];
  }

  last() {
    return this[this.index = this.max];
  }

}

const list = new List(1, 2, 3);
console.log('List:', list);
console.log('Length:', list.length);
console.log('Current value:', list.current);
console.log('Next value:', list.next());

It gets transpiled into this code that throws:

var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var List = /** @class */ (function (_super) {
    __extends(List, _super);
    function List() {
        var items = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            items[_i] = arguments[_i];
        }
        var _this = _super.apply(this, items) || this;
        Object.defineProperty(_this, 'index', {
            value: 0,
            writable: true,
            configurable: true
        });
        return _this;
    }
    Object.defineProperty(List.prototype, "current", {
        get: function () {
            return this[this.index];
        },
        enumerable: true,
        configurable: true
    });
    Object.defineProperty(List.prototype, "max", {
        get: function () {
            return this.length - 1;
        },
        enumerable: true,
        configurable: true
    });
    List.prototype.next = function () {
        return this.index === this.max
            ? this.first()
            : this[++this.index];
    };
    List.prototype.prev = function () {
        return this.index === 0
            ? this.last()
            : this[--this.index];
    };
    List.prototype.first = function () {
        return this[this.index = 0];
    };
    List.prototype.last = function () {
        return this[this.index = this.max];
    };
    return List;
}(Array));
var list = new List(1, 2, 3);
console.log('List:', list);
console.log('Length:', list.length);
console.log('Current value:', list.current);
console.log('Next value:', list.next());

Why?

Upvotes: 1

Views: 215

Answers (1)

Giacomo De Liberali
Giacomo De Liberali

Reputation: 874

I see you are targeting ES5. When transpiling to ESNext your code will perfectly work as expected. If you need to target ES5 and extend native types (such as Array) you will have to set in the constructor the prototype of your own class (Object.setPrototypeOf).

class List<T> extends Array<T> {

  constructor(...items: T[]) {
    super(...items);
    // ...
    Object.setPrototypeOf(this, Object.create(List.prototype)); // (Object as any).setPrototypeOf
  }

  // ...

}

Check the console in the playground example.

Check also another useful link on how to extend native types like Array.

Upvotes: 2

Related Questions