Safely extend the prototype in Typescript

| Tag typescript  types 

In Javascript we can modify any built-in method(like [].push, 'abc'.toUpperCase, Object.assign) at runtime. We can directly access prototypes for every built-in object – Array.prototype, Function.prototype, Object.prototype, and so on.

In the example below we add zip() to Array.prototype.

Interface

We take advantage of interface merging to augment the global Array<T> interface, adding our own zip method to the already globally defined interface:

// in zip.ts file

// Tell TS about .zip
interface Array<T> {
    zip<U>(list: U[]): [T, U][];
}

Since our file doesn’t have any explicit imports or exports—meaning it’s in script mode, we were able to augment the global Array interface directly by declaring an interface with the exact same name. See [[Module mode vs script mode in Typescript]].

If our file were in module mode, we’d have to wrap our global extension in a declare global type declaration:

// Tell TS about .zip
declare global {
    interface Array<T> {
        zip<U>(list: U[]): [T, U][];
    }
}

Implementation

We use a this type so that TS correctly infers the T type of the array we’re calling .zip on.

See [[Create a tuple constructor function in Typescript]], we use our tuple utility to create a tuple type without resorting to a type assertion:


// Implement .zip
Array.prototype.zip = function<T, U>(
    this: T[],
    list: U[]
) : [T, U][] {
    return this.map((item, idx) => tuple(item, list[idx]));
}

load implementation before using

Update our tsconfig.json to explicitly exclude zip.ts from our project, so that consumers have to explicitly import it first:

  exclude: ["./zip.ts"]
}
import './zip';
[1, 2, 3]
	.map(n => n * 2)  // number[]
	.zip(['a', 'b', 'c']); // [number, string][]

Prev     Next