6

I'm trying to extend the base Array interface with some custom methods. I looked around SO and typescript docs and finally put together the following code:

// In module Func.ts

declare global {
  type Array<T> = {
    intersperse(mkT: (ix: number) => T): T[];
  };
}

if (!('intersperse' in Array.prototype)) {
  Array.prototype.intersperse = function intersperse<T>(this: T[], mkT: (ix: number) => T): T[] {
    return this.reduce((acc: T[], d, ix) => [...acc, mkT(ix), d], []).slice(1);
  };
}

However, I'm getting the following errors:

// On type Array<T> = { ... }
Duplicate identifier 'Array'.ts(2300)

// On Array.prototype.intersperse = ...
Property 'intersperse' does not exist on type 'any[]'.ts(2339)

Also, whenever I try to use intersperse in some other file, I get the error

Property 'intersperse' does not exist on type 'Element[]'.ts(2339)

Which is to be expected, considering the declaration in Func.ts seemingly didn't work. From this I gather that the SO questions are outdated (or incomplete) and that something has changed since.

So, what's the best way to extend to get rid of these errors and extend the Array prototype? Before you say I'm not supposed to do so — yeah, I know all the risks, and I made an informed decision to do it anyway.

Eugleo
  • 418
  • 4
  • 11
  • 1
    Please read [Why is extending native objects a bad practice?](https://stackoverflow.com/questions/14034180/why-is-extending-native-objects-a-bad-practice) – str Oct 25 '20 at 10:07
  • 3
    **If** you're going to extend native prototypes, be sure that your extensions are non-enumerable by using `Object.defineProperty` with appropriate flags. – T.J. Crowder Oct 25 '20 at 10:08
  • @T.J.Crowder Good idea, I wouldn't have thought about it! Thanks. – Eugleo Oct 25 '20 at 20:13
  • 1
    @str I knew that even if I include a "I made an informed decision to do it anyway" section, someone will complain about what I'm doing. I know you meant well, but it's still funny, – Eugleo Oct 25 '20 at 20:15
  • @Eugleo I didn't complain. I just added a related and important question about this topic. StackOverflow is not just for you to get answers but it is also a knowledge base. Many future visitors will stumble upon this question and some might not know what they are doing. Hence it is important to get the context. – str Oct 25 '20 at 21:23
  • 1
    @str Fair point. Thank you for that. I'll edit my question to include the link you posted then. – Eugleo Oct 27 '20 at 07:32
  • @str That's ancient lore from JavaScript, not TypeScript. Does it even apply? – TiggerToo Sep 01 '22 at 12:08
  • @TiggerToo TypeScript compiles to JavaScript. So yes, of course it still applies. Why do you think it is "ancient lore"? Nothing has changed. Collisions can, are, and will occur. – str Sep 11 '22 at 10:29

1 Answers1

18

Array is defined as an interface not a type. Interfaces in typescript are open ended and can be added to by multiple declarations. Types do not share the same feature.

export{}
declare global {
  interface Array<T>  {
    intersperse(mkT: (ix: number) => T): T[];
  }
}

if (!Array.prototype.intersperse) {
  Array.prototype.intersperse = function intersperse<T>(this: T[], mkT: (ix: number) => T): T[] {
    return this.reduce((acc: T[], d, ix) => [...acc, mkT(ix), d], []).slice(1);
  };
}

Playground Link

As T.J. Crowder mentioned you might consider using Object.defineProperty to ensure the property is not enumerable:

export {}
declare global {
  interface Array<T>  {
    intersperse(mkT: (ix: number) => T): T[];
  }
}

if (!Array.prototype.intersperse) {
  Object.defineProperty(Array.prototype, 'intersperse', {
    enumerable: false, 
    writable: false, 
    configurable: false, 
    value: function intersperse<T>(this: T[], mkT: (ix: number) => T): T[] {
      return this.reduce((acc: T[], d, ix) => [...acc, mkT(ix), d], []).slice(1);
    }
  });
}

Playground Link

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • Thanks, I didn't know about this type/interface distinction! Most importantly, I got bitten by `typescript-eslint/consistent-type-definitions`, which promptly changed the `interface` into `type` without me noticing it. The sample code on SO had used `interface`, of course. – Eugleo Oct 25 '20 at 20:21
  • Is `!Array.prototype.intersperse` the recommended way to check for property existence in this case? Normally I'd use `in`, to silence the TS warning "property might not exist", but here it might be actually advisable to check in this way, not to inadvertently overwrite a non-enumerable property. Is my thinking correct? – Eugleo Oct 25 '20 at 20:33
  • Just FWIW, methods on the built-in prototypes are typically writable and configurable, just not enumerable. – T.J. Crowder Oct 26 '20 at 07:27
  • 2
    @T.J.Crowder the problem with `in` is a typescript one. `in` acts as a type guard for `Array.prototype`, and since `intersperse` is declared to always exist on `Array.prototype`, the type guard `!('intersperse' in Array.prototype)` will make `Array.prototype` of type `never` (since TS believes this condition will never be true as far as declared types are concerned). The `undefined` test does not act as a type guard for `Array.prototype`, only for `Array.prototype.intersperse` and since we are only assigning it, it doesn't matter what type the compiler thinks `Array.prototype.intersperse` is – Titian Cernicova-Dragomir Oct 26 '20 at 07:43
  • I recommend to properly define the export to something like `export class GlobalExtensions` and reference it in the Providers of the module – r3mark Sep 08 '22 at 02:17