6

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.

3 Answers 3

5

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.

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

2 Comments

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<T> 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.
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.
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).

Comments

1

For Sveltekit:

Create a folder in path: src/types

Create a file for your extension, in src/types/canvas.d.ts

I want a HTMLCanvasElement extension, so I call it canvas.d.ts and add resize() method.

declare global {
  interface HTMLCanvasElement {
      resize(): void;
  }
}
    
HTMLCanvasElement.prototype.resize = function (): void {
  console.log('extension method called!');
}
    
export { };

Now interface is recognized but actual implementation not, import it in src/routes/+layout.svelte

<script lang="ts">
  import "../types/canvas.d.ts";
</script>

Use it:

<script lang="ts">
  let canvas: HTMLCanvasElement;
</script>
    
<canvas bind:this={canvas} />
<svelte:window on:resize={() => canvas.resize()} />

Comments

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.