7

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.

8
  • 1
    Please read Why is extending native objects a bad practice? Commented Oct 25, 2020 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. Commented Oct 25, 2020 at 10:08
  • @T.J.Crowder Good idea, I wouldn't have thought about it! Thanks. Commented Oct 25, 2020 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, Commented Oct 25, 2020 at 20:15
  • 1
    @str Fair point. Thank you for that. I'll edit my question to include the link you posted then. Commented Oct 27, 2020 at 7:32

1 Answer 1

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

Sign up to request clarification or add additional context in comments.

5 Comments

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.
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?
Just FWIW, methods on the built-in prototypes are typically writable and configurable, just not enumerable.
@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
I recommend to properly define the export to something like export class GlobalExtensions and reference it in the Providers of the module

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.