5

I'm trying to write an extension method for Array. Whenever the program runs the code, I get Uncaught TypeError: pairs.map(...).flatten is not a function. Here's what the code looks like:

extensions.ts

declare interface Array<T> {
  flatten<T>(): T;
}

Array.prototype.flatten = function<T> () : T {
  return this.reduce(function (flat: Array<T>, toFlatten: Array<T>) {
    return flat.concat(Array.isArray(toFlatten) ? toFlatten.flatten() : toFlatten )
  }, []);
}

values.ts

export let pairs: { start: String[], end: String[] }[] = [
  { start: [ ... ], end: [ ... ] }, ...
];
export let items: String[] = pairs.map(pair => pair.start.concat(pair.end)).flatten();

I've tried changing Array.prototype.flatten = function<T> () : T { ... to Array.prototype.flatten = <T> () : T => { ... and pairs.map(pair => pair.start.concat(pair.end)).flatten() to pairs.map(pair => pair.start.concat(pair.end)).flatten<String[]>() but it didn't do anything.

I've also read somewhere here that it could have been a transpiling problem so I changed the --target compiler to ESNext, but the error has still been popping up.

b12629
  • 75
  • 1
  • 5

2 Answers2

4

Welcome to SO, b12629

What you're seeing is a runtime exception. While your type definitions provide TS with everything it needs to compile your code, you'll need to make sure to include the definition of flatten near the top of your entry file (before you invoke it), otherwise JS engine won't know it exists on the Array prototype.

Jan Klimo
  • 4,643
  • 2
  • 36
  • 42
  • Thanks! I got it to work by placing the `flatten` definition on the top of the `value.ts` file. Could I place both the `flatten` definition and the `interface Array` declaration in 1 `extensions.ts` file? If so, where should I place `extensions.ts`? I've tried placing it in the `src` folder, the `app` folder and a `models` folder I created, but I still get the runtime error. – b12629 Jun 17 '19 at 14:32
  • I'd recommend keeping your declarations (`declare ...`) in a separate file, e.g. it can be `extensions.d.ts`. Then it's totally up to you where you place the rest, as long as you extend the prototype before you use it. It could be in the same file, or another module you require before you invoke your newly defined function. – Jan Klimo Jun 18 '19 at 06:16
1

Unfortunately the way that TypeScript loads modules at compile time is significantly different to the way that Javascript loads modules at runtime (especially when using frameworks such as React or Jest that have their own Javascript module loading system). This difference in module loading system means that even though your code compiles, it may fail during runtime (and whether it fails heavily depends on the particular framework you are using and module import/exports, leading to very fragile code). As such, it is very hard to reliably use extension methods in TypeScript, so would recommend alternatives (e.g. helper method, custom class, etc).

However, if you do really want to get Extension methods working, read on.

Fundamentally this error means that TypeScript has correctly located the interface extension during compile time:

declare interface Array<T> {
  flatten<T>(): T;
}

but Javascript has not executed the call to extend the prototype at runtime:

Array.prototype.flatten = function<T> () : T {
  return this.reduce(function (flat: Array<T>, toFlatten: Array<T>) {
    return flat.concat(Array.isArray(toFlatten) ? toFlatten.flatten() : toFlatten )
  }, []);
}

Unfortunately the currently accepted answer and other related answers on StackOverflow only mention about putting the definitions in a "top-level file", which doesn't give clear enough instructions to handle all scenarios (see How to create an extension method in TypeScript for 'Date' data type, Typescript extension method compiling but not working at run time and Typescript Extend String interface Runtime Error).

To debug inconsistent behaviour, you can put a breakpoint on the prototype extension call to see when/if it is invoked. You can also examine the proto field of the extended class to check if the extension method has been created at runtime yet. Remember that due to Tree Shaking, even adding redundant import lines will not be sufficient to force JavaScript to evaluate this line.

To ensure that the prototype extension call occurs, find all your application entry points (e.g. Jest tests, web framework global, etc.) and ensure that the prototype extension call is called from each entry point (a crude solution is to have a "setup" method" that is called at the start of each entry point). Note: remember that Jest and other test libraries can mock modules, so you want to ensure that you never mock the module that calls the prototype extension. Practically this means that you should have a separate file for extensions and disable Jest automocking (or equivalent).

Woodz
  • 1,029
  • 10
  • 24