33

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?

Alexander Abakumov
  • 13,617
  • 16
  • 88
  • 129
BalaKrishnan웃
  • 4,337
  • 7
  • 30
  • 51
  • There are already similar questions: - http://stackoverflow.com/questions/12802383/extending-array-in-typescript - http://stackoverflow.com/questions/12701732/typescript-augmenting-built-in-types - http://stackoverflow.com/questions/12830647/typescript-extend-object-in-module – thSoft Jan 10 '13 at 15:15
  • You can fix this with one line of code in the ctor, see my answer below. `Object.setPrototypeOf(this, YourDerivedClass.prototype);` – Peter Wone Jun 13 '17 at 06:59

10 Answers10

44

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.

David Sherret
  • 101,669
  • 28
  • 188
  • 178
  • 1
    This does not work in typescript 2 for me, see: https://github.com/Microsoft/TypeScript/issues/13720 and for something that works: https://blog.simontest.net/extend-array-with-typescript-965cc1134b3 – Simon Epskamp Apr 27 '18 at 08:59
  • 2
    Shooting from the hip, @SimonEpskamp, your compilation target is ES5. See my answer below for why I think so. Try targeting ES6 and if it starts working this guess is confirmed. If so details of how to make it work with ES5 are in that answer. – Peter Wone May 28 '18 at 21:42
  • 1
    @SimonEpskamp Thanks for pointing that out! I had the same issue with Error a little while ago. I updated the answer to include instructions for when targeting ES5 or below. – David Sherret May 28 '18 at 21:52
12

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!!!

BrunoLM
  • 97,872
  • 84
  • 296
  • 452
Rajagopal 웃
  • 8,061
  • 7
  • 31
  • 43
  • 1
    If I want to use this.splice in a function in YArray I get a compile error. Any suggestion to get arround this? I can't seem to get anything with typecasting to work. – Flion Jan 03 '14 at 15:22
  • 1
    oh, I got it now. to be able to use all the native methods you need to implement them all in the dummy class, not just push and pop. – Flion Jan 03 '14 at 16:10
  • 1
    Dovnvoted for supplying wrong info: "I don't think there is a way to inherit existing interfaces like Array". There are tons of examples on how to do this. All that is needed is to declare your methods. See an example further down on this page. – oligofren Sep 26 '14 at 13:41
  • I had to change the constructor to ```this.push.apply(this,items); return this;``` for it to work with initial elements (like ```new XArray(1,2,3)```) – Flion Mar 19 '15 at 11:40
8

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]);
Endre Simo
  • 11,330
  • 2
  • 40
  • 49
  • 7
    You can't just dump sort() there by itself as this will currently require the implementation of all other Array functions also. Also, "Math" already exists as an interface, not a class. – James Wilkins Jul 15 '13 at 21:19
6

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.

oligofren
  • 20,744
  • 16
  • 93
  • 180
4

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.

Peter Wone
  • 17,965
  • 12
  • 82
  • 134
1

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

snipervld
  • 69
  • 1
  • 10
Jani Hyytiäinen
  • 5,293
  • 36
  • 45
  • you mean i dont want to create Xarray implementation ts ? – BalaKrishnan웃 Jan 07 '13 at 07:45
  • 1
    If you have an already working implementation of it in JS, I don't see any point in redoing it in TS and risking it's not working anymore. They didn't redo the whole JS in TypeScript, instead the typed it in lib.d.ts so it becomes usable through TS. You can also expose the custom type through an interface and declaring a variable though which you can use your JS implementation of the XArray. – Jani Hyytiäinen Jan 07 '13 at 11:04
1

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
Will Tomlins
  • 1,436
  • 16
  • 12
1

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;
Karl
  • 1,164
  • 9
  • 25
1

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.

-2

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>.

Nigh7Sh4de
  • 395
  • 2
  • 7
  • 20
  • 1
    He was asking how to extend the JS native object in TS. What you are doing is creating an interface. This has nothing to do with the actual implementation details he is asking for. – oligofren Jul 29 '15 at 10:40