1

I know that i can create a basic Array extension, something like this, which works on all arrays, no matter what the type is within that array.

export {};

declare global {
    interface Array<T> {
        isEmpty(): boolean;
    }
}

// determines if an array is empty, or if the contents of the items in the array are empty
Array.prototype.isEmpty = function() {
    if (this.length == 0) {
        return true;
    } else {
        return this.some((c) => c != null && c != '');
    }
};

But i would like to create an extension for an array that only contains a particular object? I have tried this, but gives error

import { MyObject } from './my-object';

export {};

declare global {
    interface Array<MyObject> {
        isEmpty(): boolean;
    }
}

// determines if an array is empty, or if the contents of the items in the array are empty
Array.prototype.isEmpty = function() {
    if (this.length == 0) {
        return true;
    } else {
        return this.some((c) => c != null && c != '');
    }
};

I have tried to find out by looking here but i cannot seem to figure out the correct syntax.

Any help much appreciated :)

Nitzan Tomer
  • 155,636
  • 47
  • 315
  • 299
Gillardo
  • 9,518
  • 18
  • 73
  • 141
  • You cannot do that. The generics part isn't being translated into javascript. In javascript there's no difference between arrays with different types of objects in them. – Nitzan Tomer Sep 14 '17 at 10:50

2 Answers2

2

What are you trying to do? As @NitzanTomer points out you can't specialize generics like that in TypeScript, where Array<MyObject> has a method isEmpty(), but Array<string> does not. Depending on how you plan to use it, there are things that get you close.

Option 1: Extend Array prototype as you requested (not recommended)

The closest I can get to what you specifically asked for is to require the this parameter of isEmpty() to be of type Array<MyObject>:

declare global {
  interface Array<T> {
    isEmpty(this: Array<MyObject>): boolean;
  }
}

// note the error below
Array.prototype.isEmpty = function() {
  if (this.length == 0) {
    return true;
  } else {
    return this.some((c) => c != null &&
      c != '' // error! c has to be MyObject, not string
    );
  }
};

(As an aside, your implementation of isEmpty() has an error when you compare an array element, now required to be MyObject, to a string value. The good news is that TypeScript is alerting you of this so you can fix it.)

You can now go ahead and call isEmpty() on arrays of MyObject:

declare let myObject1: MyObject;
declare let myObject2: MyObject;
const arrayOfMyObject: MyObject[] = [myObject1, myObject2];
arrayOfMyObject.isEmpty(); // okay

Now, the isEmpty() method exists on all arrays, but trying to call it on an array of a different type will give you an error:

const arrayOfStrings: string[] = ['a', 'b'];
arrayOfStrings.isEmpty();  // error, string is not MyObject

So that might work for you.

That being said, extending native prototypes like Array is almost always considered a bad idea.


Option 2: Extend selected Array instances with new method

A better idea would be to create a brand new class with the extra method, or to take individual Array instances and add the method to them without polluting the Array prototype. Here's a way to do the latter:

interface MyObjectArray<T extends MyObject> extends Array<T>{ 
  isEmpty(): boolean;
}

const isEmpty = function(this: Array<MyObject>) {
  if (this.length == 0) {
    return true;
  } else {
    return this.some((c) => c != null &&
      c != '' // error!  c is MyObject, not string
    );
  }
};

function toMyObjectArray<T extends MyObject>(arr: Array<T>): MyObjectArray<T> {
  const ret = arr as MyObjectArray<T>;
  ret.isEmpty = isEmpty.bind(ret);
  return ret;
}

Now we have a new interface called MyObjectArray which includes the extra method. And if you have an existing instance of MyObject[], you can convert it to MyObjectArray using the toMyObjectArray() function, which adds the isEmpty() method to it. Then you can use it like this:

declare let myObject1: MyObject;
declare let myObject2: MyObject;
const arrayOfMyObject = [myObject1, myObject2];
arrayOfMyObject.isEmpty(); // error, no such method
const myObjectArray = toMyObjectArray(arrayOfMyObject); // convert
myObjectArray.isEmpty(); // okay

// can't convert an array of anything else
const arrayOfStrings = toMyObjectArray(['a', 'b', 'c']);

There's an extra step of calling a conversion function every time you need to use isEmpty(), which might make this less desirable for you.


Option 3: Use standalone isEmpty() function, not method

In fact, if you are not going to mess with Array.prototype, and you have to call a function on the array instance, you might as well just skip the extra interface and use a standalone isEmpty() instead of bothering with methods:

const isMyObjectArrayEmpty = function(arr: Array<MyObject>) {
  if (arr.length == 0) {
    return true;
  } else {
    return arr.some((c) => c != null &&
      c != '' // error!  c is MyObject, not string
    );
  }
};
    
declare let myObject1: MyObject;
declare let myObject2: MyObject;
const arrayOfMyObject = [myObject1, myObject2];
isMyObjectArrayEmpty(arrayOfMyObject); // okay

const arrayOfStrings = ['a', 'b', 'c'];
isMyObjectArrayEmpty(arrayOfStrings); // error

Those are the options as I see them.

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • I don't know what is the problem with your first option. I've strongly typed everything and then I'm not able to use the newly defined function on array of other type than the one I've defined. So it's ok. Thanks a lot :) :) :) – Nicolas Jun 29 '18 at 07:54
  • 1
    @Nicolas, see [this question](https://stackoverflow.com/questions/14034180/why-is-extending-native-objects-a-bad-practice) for some discussion about what the problem is. – jcalz Jun 29 '18 at 15:45
0

You cannot extend an array for a particular type using this method. Typescript uses generic type erasure, so at runtime there is no way to distinguish between Array<A> and Array<B>. You could create a derived array type that adds the required methods, sample code here, but you would have to new up that class instead of creating simple arrays with []

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • Runtime erasure is not the issue here : we would just like to know *at compile time* that the extension method can be called on an array of `MyObject` but not on an array of any other type. – laurent Jul 13 '20 at 16:11