简体   繁体   中英

Extending Built-in Array With ES6 Classes - Method is not a function error

I'm trying to extend built-in array class and create custom collections based native array class.

I'm able to create collection with custom name, it works well, but I can't use my custom methods. If I use findAnimal method it gives me method is not a function error. If I log collection and inspect prototypes I can't see my custom methods.

I realized that I transpile down the code into the es5 and if I not, works well, Problem occurs with the ts compiler's es5 downgraded code.

If we use babel to downgrade code into es5, transpiled code works well. Either ts compiler missing something when transpiling the code or I missing some vital configuration.

interface Animal {
    name: string;
    weight: number
}

class AnimalCollection extends Array <Animal> {
    constructor(name, ...items) {
        super(...items);

        Object.defineProperty(this, 'name', {
            enumerable: false,
            writable: false,
            value: name
        })
    }

    findAnimal(name): Animal {
        return this.find(a => a.name === name) || null;
    }
}

const animalsArray = [
    {name: 'TD-23', weight: 60},
    {name: 'TD-25', weight: 50},
    {name: 'TXD-26', weight: 120},
    {name: 'TYD-26', weight: 40}
];

const animals = new AnimalCollection('Deers', ...animalsArray)

console.log(animals.findAnimal('TD-23'));
// Uncaught TypeError: animals.findAnimal is not a function

ES5 Downgraded

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 __spreadArrays = (this && this.__spreadArrays) || function () {
    for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
    for (var r = Array(s), k = 0, i = 0; i < il; i++)
        for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
            r[k] = a[j];
    return r;
};

var AnimalCollection = /** @class */ (function (_super) {
    __extends(AnimalCollection, _super);
    function AnimalCollection(name) {
        var items = [];
        for (var _i = 1; _i < arguments.length; _i++) {
            items[_i - 1] = arguments[_i];
        }
        var _this = _super.apply(this, items) || this;
        Object.defineProperty(_this, 'name', {
            enumerable: false,
            writable: false,
            value: name
        });
        return _this;
    }
    AnimalCollection.prototype.findAnimal = function (name) {
        return this.find(function (a) { return a.name === name; }) || null;
    };
    return AnimalCollection;
}(Array));
var animalsArray = [
    { name: 'TD-23', weight: 60 },
    { name: 'TD-25', weight: 50 },
    { name: 'TXD-26', weight: 120 },
    { name: 'TYD-26', weight: 40 }
];
var animals = new (AnimalCollection.bind.apply(AnimalCollection, __spreadArrays([void 0, 'Deers'], animalsArray)))();
console.log(animals.findAnimal('TD-23'));

UPDATED ANSWER

To answer your modified question, TypeScript doesn't let you extend built-ins like Array , Error etc. The reason is written here .

In ES2015, constructors which return an object implicitly substitute the value of this for any callers of super(...). It is necessary for generated constructor code to capture any potential return value of super(...) and replace it with this.

As a result, subclassing Error, Array, and others may no longer work as expected. This is due to the fact that constructor functions for Error, Array, and the like use ECMAScript 6's new.target to adjust the prototype chain; however, there is no way to ensure a value for new.target when invoking a constructor in ECMAScript 5. Other downlevel compilers generally have the same limitation by default.

So if you must extend built-in Array under ES5 environment, then you can try Babel for compiling your code. However be aware that it has it's limitation as stated here .

Built-in classes such as Date, Array, DOM etc cannot be properly subclassed due to limitations in ES5 (for the transform-classes plugin). You can try to use babel-plugin-transform-builtin-extend based on Object.setPrototypeOf and Reflect.construct, but it also has some limitations.

OLD ANSWER

While the code itself is perfectly find and also executes fine in the browser, I think the error you are getting is because of the TypeScript compiler.

For the source code

interface Animal {
    name: string;
    weight: number
}

class AnimalCollection extends Array <Animal> {
    constructor(name: string, ...items: Animal[]) {
        super(...items);

        Object.defineProperty(this, 'name', {
            enumerable: false,
            writable: false,
            value: name
        })
    }

    findAnimal(name:string): Animal | null {
        return this.find(a => a.name === name) || null;
    }
}

const animalsArray = [
    {name: 'TD-23', weight: 60},
    {name: 'TD-25', weight: 50},
    {name: 'TXD-26', weight: 120},
    {name: 'TYD-26', weight: 40}
];

const animals = new AnimalCollection('Deers', ...animalsArray)

console.log(animals.findAnimal('TD-23'));

If the compiler target option is set to ES5 , then it produces code that breaks the implementation. The code it generates is

"use strict";
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 __spreadArrays = (this && this.__spreadArrays) || function () {
    for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
    for (var r = Array(s), k = 0, i = 0; i < il; i++)
        for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
            r[k] = a[j];
    return r;
};
var AnimalCollection = /** @class */ (function (_super) {
    __extends(AnimalCollection, _super);
    function AnimalCollection(name) {
        var items = [];
        for (var _i = 1; _i < arguments.length; _i++) {
            items[_i - 1] = arguments[_i];
        }
        var _this = _super.apply(this, items) || this;
        Object.defineProperty(_this, 'name', {
            enumerable: false,
            writable: false,
            value: name
        });
        return _this;
    }
    AnimalCollection.prototype.findAnimal = function (name) {
        return this.find(function (a) { return a.name === name; }) || null;
    };
    return AnimalCollection;
}(Array));
var animalsArray = [
    { name: 'TD-23', weight: 60 },
    { name: 'TD-25', weight: 50 },
    { name: 'TXD-26', weight: 120 },
    { name: 'TYD-26', weight: 40 }
];
var animals = new (AnimalCollection.bind.apply(AnimalCollection, __spreadArrays([void 0, 'Deers'], animalsArray)))();
console.log(animals.findAnimal('TD-23'));

However, if we set the target in tsconfig.json to something equal to or greater than ES2015 , then the code it generates is

"use strict";
class AnimalCollection extends Array {
    constructor(name, ...items) {
        super(...items);
        Object.defineProperty(this, 'name', {
            enumerable: false,
            writable: false,
            value: name
        });
    }
    findAnimal(name) {
        return this.find(a => a.name === name) || null;
    }
}
const animalsArray = [
    { name: 'TD-23', weight: 60 },
    { name: 'TD-25', weight: 50 },
    { name: 'TXD-26', weight: 120 },
    { name: 'TYD-26', weight: 40 }
];
const animals = new AnimalCollection('Deers', ...animalsArray);
console.log(animals.findAnimal('TD-23'));

Which of course works. So I think there is an issue in TypeScript compiler for version ES5 or less, which breaks the implementation. I have tried using Babel for compilation and it works for ES5.

I can see that you was missing some any , this is the code that works for me:

interface Animal {
    name: string;
    weight: number
}

class AnimalCollection extends Array <Animal> {
    constructor(name: string, ...items : Array <Animal>) { // <-- missing types
        super(...items);

        Object.defineProperty(this, 'name', {
            enumerable: false,
            writable: false,
            value: name
        })
    }

    findAnimal(name : any): Animal|null { // <-- missing null
        return this.find(a => a.name === name) || null;
    }
}

const animalsArray = [
    {name: 'TD-23', weight: 60},
    {name: 'TD-25', weight: 50},
    {name: 'TXD-26', weight: 120},
    {name: 'TYD-26', weight: 40}
];

const animals = new AnimalCollection('Deers', ...animalsArray)

console.log(animals.findAnimal('TD-23'));

However this code generates the following JS:

"use strict";
class AnimalCollection extends Array {
    constructor(name, ...items) {
        super(...items);
        Object.defineProperty(this, 'name', {
            enumerable: false,
            writable: false,
            value: name
        });
    }
    findAnimal(name) {
        return this.find(a => a.name === name) || null;
    }
}
const animalsArray = [
    { name: 'TD-23', weight: 60 },
    { name: 'TD-25', weight: 50 },
    { name: 'TXD-26', weight: 120 },
    { name: 'TYD-26', weight: 40 }
];
const animals = new AnimalCollection('Deers', ...animalsArray);
console.log(animals.findAnimal('TD-23'));

Which does not generate any error

it's how you wrote your function that causes the error

 interface Animal { name: string; weight: number } class AnimalCollection extends Array <Animal> { constructor(name, ...items) { super(...items); Object.defineProperty(this, 'name', { enumerable: false, writable: false, value: name }) } findAnimal:Animal=(name:String)=> { return this.find(a => a.name === name) || null; } } const animalsArray = [ {name: 'TD-23', weight: 60}, {name: 'TD-25', weight: 50}, {name: 'TXD-26', weight: 120}, {name: 'TYD-26', weight: 40} ]; const animals = new AnimalCollection('Deers', ...animalsArray) console.log(animals.findAnimal('TD-23')); // Uncaught TypeError: animals.findAnimal is not a function

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM