Search code examples
typescriptmodule-augmentation

Typescript how to augment Object class?


I am familiar with the Kotlin extension function let (and related) (cf. https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/let.html).

I wanted to use something similar in TypeScript, using augmentation (or is it called declaration merging?). So I thought I can add the let method to the Object superclass.

How can I do it? I thought of something like the following, but that didn't work. Typescript doesn't seem to augment, but to solely use the following interface.

interface Object {
  let<T, R>(block: (t: T) => R): R
}

(Object.prototype as any).let = function<T, R>(block: (t: T) => R) {
  return block(this)
}

EDIT:

Test cases would be:

'42'.let(it => alert(it))
'foo'.let(it => it.toUpperCase())

(i. e. let would be available on any objects, in this case strings)


Solution

  • TL;DR

    interface Object {
      let<T, R>(this: T, block: (t: T) => R): R;
    }
    
    Object.prototype.let = function <T, R>(this: T, block: (t: T) => R): R {
      return block(this);
    };
    
    '42'.let(it => alert(it)); // let -> Object.let<string, void>(this: string, block: (t: string) => void): void
    (42).let(it => alert(it)); // let -> Object.let<number, void>(this: number, block: (t: number) => void): void
    'foo'.let(it => it.toUpperCase()); // let -> Object.let<string, string>(this: string, block: (t: string) => string): string
    ['foo'].let(it => it[0].toUpperCase()); // let -> Object.let<string[], string>(this: string[], block: (t: string[]) => string): string
    

    codepan

    EDIT 1

    Updated the above to reflect better typing of the this argument + examples

    EDIT 2

    Explanation

    1. declaring let on the Object interface merges that declaration with the rest of the Object interface. See Declaration merging
    2. In case this declaration is in a module (a file with import and/or export) you do the declaration in a global scope:
      declare global {
        interface Object {
          ...
        }
      }
      
      See Global augmentation
    3. Using the this argument in the method declaration declares the type of this in the method signature. See Polymorphic thistypes
    4. TypeScript needs a way to implicitly understand the specific type, to do so, we'll use generics. See Generics
    5. Putting all of the above together, by declaring method<T>(this: T), we make TypeScript know that the this argument should take the form of the type the method is executed on. That way, if the method exists on a type (and it does, since we augmented the Object interface), using it on that type causes the this argument to be of that type.