Extend native JavaScript array

Is there any way to inherit a class from JS native function?

For example, I have a JS function like this:

function Xarray()
{
    Array.apply(this, arguments);
    //some stuff for insert, add and remove notification
}
Xarray.prototype = new Array();

I tried to convert it to Typescript but i failed!!

export class Xarray implements Array {
}

The compiler asks me to define all Array interface properties. I know if I need this Xarray.prototype = new Array();, I have to extend Array in TS.

How to extend the JS native object in TS?

Answers:

Answer

Starting in TypeScript 1.6, you can extend the Array type, see What's new in TypeScript

Here's an example:

class MyNewArray<T> extends Array<T> {
    getFirst() {
        return this[0];
    }
}

var myArray = new MyNewArray<string>();
myArray.push("First Element");
console.log(myArray.getFirst()); // "First Element"

If you are emitting to ES5 or below, then use the following code:

class MyNewArray<T> extends Array<T> {
    constructor(...items: T[]) {
        super(...items);
        Object.setPrototypeOf(this, MyNewArray.prototype);
    }

    getFirst() {
        return this[0];
    }
}

Read more about why this is necessary here.

Answer

Yes it's possible to extend a native JS object in TS, however there is an issue extending built-in types (those included in lib.d.ts) like Array. Read this post for workaround: http://typescript.codeplex.com/workitem/4

So defining a type interface which extends a native type object at a later stage can be done in the following way:

/// <reference path="lib.d.ts"/>
interface Array {
    sort: (input: Array) => Array;
}

Using on a concrete example, you can sort some elements on an array which define a sort function in an interface and later implements it on an object.

class Math implements Array {
    sort : (x: Array) => Array {
          // sorting the array
    }
}
var x = new Math();
x.sort([2,3,32,3]);
Answer

While researching this, I came across Ben Nadel's excellent post on Extending JavaScript Arrays While Keeping Native Bracket-Notation Functionality. After some initial confusion on how to succesfully convert this into TypeScript, I created a fully working Collection class that can be subclassed.

It can do everything an Array can, including indexing by brackets,use in loop constructions (for, while, forEach), maps, etc.

The main implementation points are

  1. Create an array in the constructor, add the methods to the array and return that from the constructor
  2. Copy dummy declarations of Array methods to pass the implements Array bit

Example of usage:

  var foo = new Foo({id : 1})
  var c = new Collection();

  c.add(foo)
  c.length === 1;    // => true

  foo === c[0];      // => true
  foo === c.find(1); // => true

I made it available as a gist, complete with tests and an example implementation of a subclass, but I present the full source here:

/*
 * Utility "class" extending Array with lookup functions
 *
 * Typescript conversion of Ben Nadel's Collection class.
 * https://gist.github.com/fatso83/3773d4cb5f39128b3732
 *
 * @author Carl-Erik Kopseng
 * @author Ben Nadel (javascript original)
 */

export interface Identifiable {
    getId : () => any;
}

export class Collection<T extends Identifiable> implements Array<T> {

    constructor(...initialItems:any[]) {
        var collection = Object.create(Array.prototype);

        Collection.init(collection, initialItems, Collection.prototype);

        return collection;
    }

    static init(collection, initialItems:any[], prototype) {
        Object.getOwnPropertyNames(prototype)
            .forEach((prop) => {
                if (prop === 'constructor') return;

                Object.defineProperty(collection, prop, { value: prototype[prop] })
            });

        // If we don't redefine the property, the length property is suddenly enumerable!
        // Failing to do this, this would fail: Object.keys([]) === Object.keys(new Collection() )
        Object.defineProperty(collection, 'length', {
            value: collection.length,
            writable: true,
            enumerable: false
        });

        var itemsToPush = initialItems;
        if (Array.isArray(initialItems[0]) && initialItems.length === 1) {
            itemsToPush = initialItems[0];
        }
        Array.prototype.push.apply(collection, itemsToPush);

        return collection;
    }

    // Find an element by checking each element's getId() method
    public find(id:any):T;

    // Find an element using a lookup function that
    // returns true when given the right element
    public find(lookupFn:(e:T) => boolean):T ;

    find(x:any) {
        var res, comparitor;

        if (typeof x === 'function') {
            comparitor = x;
        } else {
            comparitor = (e) => {
                return e.getId() === x;
            }
        }

        res = [].filter.call(this, comparitor);

        if (res.length) return res[0];
        else return null;
    }

    // Add an element
    add(value:T);

    // Adds all ements in the array (flattens it)
    add(arr:T[]);

    add(arr:Collection<T>);

    add(value) {

        // Check to see if the item is an array or a subtype thereof
        if (value instanceof Array) {

            // Add each sub-item using default push() method.
            Array.prototype.push.apply(this, value);

        } else {

            // Use the default push() method.
            Array.prototype.push.call(this, value);

        }

        // Return this object reference for method chaining.
        return this;

    }

    remove(elem:T):boolean;

    remove(lookupFn:(e:T) => boolean):boolean ;

    remove(x:any):boolean {
        return !!this._remove(x);
    }

    /**
     * @return the removed element if found, else null
     */
    _remove(x:any):T {
        var arr = this;
        var index = -1;

        if (typeof x === 'function') {

            for (var i = 0, len = arr.length; i < len; i++) {
                if (x(this[i])) {
                    index = i;
                    break;
                }
            }

        } else {
            index = arr.indexOf(x);
        }

        if (index === -1) {
            return null;
        }
        else {
            var res = arr.splice(index, 1);
            return res.length ? res[0] : null;
        }
    }


    // dummy declarations
    // "massaged" the Array interface definitions in lib.d.ts to fit here
    toString:()=> string;
    toLocaleString:()=> string;
    concat:<U extends T[]>(...items:U[])=> T[];
    join:(separator?:string)=> string;
    pop:()=> T;
    push:(...items:T[])=> number;
    reverse:()=> T[];
    shift:()=> T;
    slice:(start?:number, end?:number)=> T[];
    sort:(compareFn?:(a:T, b:T) => number)=> T[];
    splice:(start?:number, deleteCount?:number, ...items:T[])=> T[];
    unshift:(...items:T[])=> number;
    indexOf:(searchElement:T, fromIndex?:number)=> number;
    lastIndexOf:(searchElement:T, fromIndex?:number)=> number;
    every:(callbackfn:(value:T, index:number, array:T[]) => boolean, thisArg?:any)=> boolean;
    some:(callbackfn:(value:T, index:number, array:T[]) => boolean, thisArg?:any)=> boolean;
    forEach:(callbackfn:(value:T, index:number, array:T[]) => void, thisArg?:any)=> void;
    map:<U>(callbackfn:(value:T, index:number, array:T[]) => U, thisArg?:any)=> U[];
    filter:(callbackfn:(value:T, index:number, array:T[]) => boolean, thisArg?:any)=> T[];
    reduce:<U>(callbackfn:(previousValue:U, currentValue:T, currentIndex:number, array:T[]) => U, initialValue:U)=> U;
    reduceRight:<U>(callbackfn:(previousValue:U, currentValue:T, currentIndex:number, array:T[]) => U, initialValue:U)=> U;
    length:number;
[n: number]: T;
}

Of course, the bits on Identifiable, the find and remove methods are not needed, but I supply them none the less as a full fledged example is a tad more usable than a bare-bones Collection without any methods of its own.

Answer

Constructors that return an object implicitly substitute the value of this for callers of super(). Generated constructor code has to capture whatever super() returns and replace it with this.

Built-in classes use ES6 new.target to do the fixup but there's no way for ES5 code to ensure that new.target has a value calling the constructor.

This is why your extra methods vanish - your object has the wrong prototype.

All you need to do is fix the prototype chain after calling super().

export class RoleSet extends Array {
  constructor() {
    super();
    Object.setPrototypeOf(this, RoleSet.prototype);
  }
  private containsRoleset(roleset:RoleSet){
      if (this.length < roleset.length) return false;
      for (var i = 0; i < roleset.length; i++) {
        if (this.indexOf(roleset[i]) === -1) return false;
      }
      return true;
  }
  public contains(item: string | RoleSet): boolean {
    if (item) {
      return typeof item === "string" ? 
        this.indexOf(item) !== -1 : 
        this.containsRoleset(item);
    } else {
      return true;
    }
  }
}

Be aware that this curse shall afflict thy children and thy children's children until the end of code; you have to do the fixup in every generation of an inheritance chain.

Answer

In your case, a good bet would be to use this pattern:

function XArray(array) {
  array = array || [];

  //add a new method
  array.second = function second() {
    return array[1];
  };

  //overwrite an existing method with a super type pattern
  var _push = array.push;
  array.push = function push() {
    _push.apply(array, arguments);
    console.log("pushed: ", arguments);
  };

  //The important line.
  return array
}

Then you can do:

var list = XArray([3, 4]);
list.second()   ; => 4

list[1] = 5;
list.second()   ; => 5

note however that:

list.constructor  ; => Array and not XArray
Answer

Yes you can augment the Builtin types and do it in a way that doesn't require all the paraphernalia of an XArray as described in the other answers and is closer to how you would do it in javascript.

Typescript allows a number of ways to do this, but for the Builtin types like Array and Number you need to use "merging" and declare the global namespace to augment the types, see the docs

so for Array we can add an optional metadata object and a get first member

declare global {
  interface Array<T> {
    meta?: any|null ,
    getFirst(): T
  }
}

if(!Array.prototype.meta )
{
  Array.prototype.meta = null
}
if(!Array.prototype.getFirst )
{
  Array.prototype.getFirst = function() {
    return this[0];
  }
}

we can use this like so:

let myarray: number[] = [ 1,2,3 ]
myarray.meta = { desc: "first three ints" }
let first: number = myarray.getFirst()

The same goes for Number say I want to add a modulo function that isn't limited like the remainder %

declare global {
  interface Number {
    mod(n:number): number
  }
}

if(!Number.prototype.mod )
{
  Number.prototype.mod = function (n: number) {
          return ((this % n) + n) % n;
  }
}

and we can use it like so:

let foo = 9;
console.log("-9.mod(5) is "+ foo.mod(5))    

For Functions that we may want to add an API to ie to make it behave like a function and an object we can use hybrid types (see docs)

// augment a (number) => string  function with an API
interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

//helper function to get my augmented counter function with preset values
function getCounter(): Counter {
    let counter = <Counter>function (start: number) { };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

use it like so:-

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
Answer

With purpose to overcome the problem of extension of the native Array class, I took advantage of a decorator.

function extendArray(constructor: Function) {
    Object.getOwnPropertyNames(constructor.prototype)
        .filter(name => name !== 'constructor')
.forEach(name => {
    const attributes = Object.getOwnPropertyDescriptor(constructor.prototype, name);
    Object.defineProperty(Array.prototype, name, attributes);
  });
}

@extendArray
export class Collection<T> extends Array<T> {
  constructor(...args: T[]) {
    super(...args);
  }
  // my appended methods
}

BTW This decorator can be made more generic (for other native classes) if to use a decorator factory.

Answer

Don't know how frowned upon this is but for example I needed to create an array of BulletTypes so that I could cycle through them. What I did is the following:

interface BulletTypesArray extends Array<BulletType> {
    DefaultBullet?: Bullet; 
}

var BulletTypes: BulletTypesArray = [ GreenBullet, RedBullet ];
BulletTypes.DefaultBullet = GreenBullet;

Obviously you could could also make a generic interface, something like interface SuperArray<T> extends Array<T>.

Answer

I don't think there is a way to inherit existing interfaces like Array,

export class Xarray implements Array {

}

You should create a function and inherit it with its prototype. Typescript also will accept it which is similar to javascript.

function Xarray(...args: any[]): void; // required in TS 0.9.5
function Xarray()
{
    Array.apply(this, arguments);
   // some stuff for insert, add and remove notification
}
Xarray.prototype = new Array();

UPDATE: This one is discussed well and provided the best solution for this at jqfaq.com.

//a dummy class it to inherite array.
class XArray {
    constructor() {
        Array.apply(this, arguments);   
        return new Array();
    }
    // we need this, or TS will show an error,
    //XArray["prototype"] = new Array(); will replace with native js arrray function
    pop(): any { return "" };
    push(val): number { return 0; };
    length: number;
}
//Adding Arrray to XArray prototype chain.
XArray["prototype"] = new Array();

//our Class
class YArray extends XArray {
///Some stuff
}

var arr = new YArray();
//we can use the array prop here.
arr.push("one");
arr.push("two");

document.writeln("First Elemet in array : " + arr[0]);
document.writeln("</br>Array Lenght : " + arr.length);

Hope, this might help you!!!

Answer

If you already have a working Xarray implementation, I don't see the point in recreating it in typescript, which eventually will compile back to JavaScript.

But I do see the point in being able to use the Xarray in TypeScript.

In order to accomplish this, you simply need an interface for your Xarray. You don't even need to have a concrete implementation of your interface since your existing js implementation will serve as one.

interface Xarray{
    apply(...arguments : any[]) : void;
    //some stuff for insert, add and ...
}
declare var Xarray: {
   new (...items: any[]): Xarray;
   (...items: any[]): Xarray;
   prototype: Array; // This should expose all the Array stuff from ECMAScript 
}

After doing this, should be able to use your custom defined type through the declared variable without actually implementing it in TypeScript.

var xArr = new Xarray();
xArr.apply("blah", "hehe", "LOL");

You might look for reference here to see how they typed the ECMAScript Array API: http://typescript.codeplex.com/SourceControl/changeset/view/2bee84410e02#bin/lib.d.ts

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.