Typescript: Scoped augments / monkey patches / refinements

Created on 4 May 2016  Â·  10Comments  Â·  Source: microsoft/TypeScript

Now that augments / monkey patches are live in 1.8, maybe we could consider a move towards safer augments that are scoped to the file they're used in.

As a current workaround, I'm using explicit conversions, which are a Typescript take on implicit classes. (The difference being you have to explicitly invoke the conversion). They provide _scoped_ monkey-patching. Here is some sample code written in this style.

Would these be considered as a future addition to TypeScript with nicer syntax and direct compiler support?

Awaiting More Feedback Suggestion

Most helpful comment

@aluanhaddad
I'd say monkey patching is a long-standing, but frowned upon, JavaScript tradition 😛
This approach lets us eliminate its dark side by reconciling and type-checking the monkey patches at compile-time, avoiding errors and performance gotchas.

All 10 comments

Can you provide more details of the proposal?

I'm a big fan of Scala and while the ability to extend existing types in a non-prototype-polluting way would provide a very meaningful level of abstraction I'm not sure that implicit classes would be a good fit for TypeScript as they may not map well to JavaScript.

I assume that you are proposing something like this

implicit class ArrayWithGroupBy<T> {
    constructor(private array: T[]) {}

    groupBy<TKey>(keySelector: (value: T) => TKey): Map<TKey, T[]> {
        return this.array.reduce((groups, value) => {
            const key = keySelector(value);
            if (!groups.has(key)) {
                groups.set(key, [value]);
            } 
            else {
                groups.get(key).push(value);
            }
            return groups;
        }, new Map<TKey, T[]>());
    }
}

...

let colors = ["red", "ruby", "blue", "teal", "tan", "beige"];

// translation : new ArrayWithGroupBy(colors).groupBy(color => color[0]);
let byLength = colors.groupBy(color => color[0]); 

console.info(byLength); // Map {"r" => ["red", "ruby"], "b" => ["blue", "beige"], "t" => ["teal", "tan"]}

This would be super useful, but it doesn't seem very JavaScript.
Still this is an interesting idea.

Edit: Fixed types; fixed output

Abstract

A new language construct is proposed to create functions which provide extensions to another type. These are similar to augmentations, but are limited to their initial scope.

Description

The implicit keyword will now be allowed on functions. Functions annotated with the implicit keyword are referred to as _implicit conversions_.

An _implicit conversion_ must have exactly one argument and must return an instance of an Object.

The _implicit conversion_ is only active in the scope where it's been declared or imported. _Implicit conversions_ do not carry over across files or through function invocations.

When an _implicit conversion_ is active, any function or property resolution errors will not immediately cause compilation to fail. The _implicit conversion_ will attempt to apply itself to the object failing compilation, returning a new object. This object is then re-tested for resolution, and that result is final.

Usage

To create an _implicit conversion_, place the implicit keyword in front of a function declaration and return an extended object from that function:

/* rich_string_implicits.ts */
class RichString extends String {
    get capitalized(): string {
        return this[0].toUpperCase() + this.slice(1);
    }
}

implicit function RichStringImplicits(s: string): RichString {
  return new RichString(s);
}

export default RichStringImplicits;

This example creates an _implicit conversion_ that wraps a String object and extends it with a new getter, capitalized. To use this conversion, import it into scope and call the capitalized getter. Here's an example:

/* person.ts */
import RichStringImplicits from './rich_string_implicits.ts';

class Person {
  firstName: string
  lastName: string

  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  get fullName(): string {
    return this.firstName.capitalized + this.lastName.capitalized;
  }
}

Specification

The language specification (6.1) would be modified to allow the use of the implicit modifier for functions.

  _FunctionDeclaration: ( Modified )
   implicitopt function BindingIdentifieropt CallSignature { FunctionBody }
   implicitopt function BindingIdentifieropt CallSignature ;_

Consequences

The new syntax should not break existing code.

Having several active _implicit conversions_ may slow down compilation in code with many errors.

_Implicit conversions_ and augmentations may need to merge to avoid syntax bloat.

This is a proposal for implicit conversions - implicit classes are a small layer of syntax sugar on top of this ^.

Bump! Any comments?

This looks like a library feature and not a compiler transformation. so i am inclined to say this is out of scope of the TS project. leaving the issue open for now to get more feedback.

I'm not sure this can be implemented as a library - what did you have in mind?

Bump! As of now, only interfaces/namespaces/modules can be patched (per merging rules).

A great use case for this is for mocking complex dependencies. Currently, there's no good way to monkey patch dependencies of dependencies reliably - but this solved by this proposal.

@aluanhaddad
I'd say monkey patching is a long-standing, but frowned upon, JavaScript tradition 😛
This approach lets us eliminate its dark side by reconciling and type-checking the monkey patches at compile-time, avoiding errors and performance gotchas.

There doesn't seem to be much demand for this; closing.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

blendsdk picture blendsdk  Â·  3Comments

seanzer picture seanzer  Â·  3Comments

CyrusNajmabadi picture CyrusNajmabadi  Â·  3Comments

bgrieder picture bgrieder  Â·  3Comments

siddjain picture siddjain  Â·  3Comments