Search code examples
genericstypescriptprototype

Can typescript extend the Array.prototype with methods that use the element type parameter?


That was a very long title to describe something quite simple.

interface Array<T> {
  sortBy<T, K>(keyFunction: (t: T) => K): T[];
}

.

export namespace util {
export function sortBy<Element, Key>(array: Element[], keyFunction: (t: Element) => Key ): Element[] {

    var arrayCopy = array.slice();

    var mapped = arrayCopy.map(function(el, i) {
      return { index: i, value: keyFunction(el) };
    });

    mapped.sort(function(a, b) {
      return +(a.value > b.value) || +(a.value === b.value) - 1;
    });

    for (let i = 0; i < array.length; i++) {
        let indexed = mapped[i];
        array[i] = arrayCopy[indexed.index];
    }

    var result = mapped.map(function(el){
      return arrayCopy[el.index];
    });

    return array;

}
}

.

Array.prototype.sortBy = function(keyFunction)  {return util.sortBy(this, keyFunction)}

Here I am implementing some sample code from MDN to sort by an array key instead of comparison function, and attempting to patch the Array prototype (comments on whether this is a good idea or not are expected) with it.

However, when I call it, Typescript can't verify the proper argument types of keyfunction, and thus a cast is required to prevent a compiler error:

it('should sort in-place', () => {
    var list = [{a: 'z', i: 2}, {a: 'x', i: 0}, {a: 'y', i:1}];
    // all good
    list.sortBy(<Function>(obj) => obj.i);
    expect(list.map((obj) => obj.a)
               .join('')
    ).toBe('xyz');
})


it('should sort in-place', () => {
    var list = [{a: 'z', i: 2}, {a: 'x', i: 0}, {a: 'y', i:1}];
    // Error TS2339: Property 'i' does not exist on type '{}'
    list.sortBy((obj) => obj.i);
    expect(list.map((obj) => obj.a)
               .join('')
    ).toBe('xyz');
})

Is there any way I can change this definition to work? Interestingly, calling util.sortBy(array, function) (i.e. as a function, not a method) has type parameters working correctly.


Solution

  • Your interface declaration's sort function is creating a new T (sortBy<T, K>) disconnected from the Array's T. Should be sortBy<K> and reuse the Array's T.

    Correct Declaration

    interface Array<T> {
      sortBy<K>(keyFunction: (t: T) => K): T[];
    }
    
    var list = [{a: 'z', i: 2}, {a: 'x', i: 0}, {a: 'y', i:1}];
    
    // Okay
    list.sortBy((obj) => obj.i);