Typescript: Allow functions to implicitly perform interface merging on class inputs

Created on 17 Jun 2017  路  14Comments  路  Source: microsoft/TypeScript



TypeScript Version: 2.4.0 / nightly (2.5.0-dev.201xxxxx)

New proposal

Code

Consider the following class decorators

function mappable(Class){
    Class.prototype.mapResponse = function(){}
}

function validated(Class) : any {
    return class extends Class {
        checkValidation(){}
    }
}

@mappable
@validated
class Foo{
    save(){
        if (!this.checkValidation()){ return; }
    }
    load(){
        fetch('foo').then(resp => this.mapResponse(resp));
    }
}

This of course produces TypeErrors on this.checkValidation and this.mapResponse even though this code will work at runtime.

Currently, the best workaround is to have these decorators export needed interfaces, which need to be manually merged with the class being decorated

function mappable(Class){
    Class.prototype.mapResponse = function(){}
}
interface IMappable{
    mapResponse(obj : any);
}

function validated(Class) : any {
    return class extends Class {
        checkValidation(){}
    }
}
interface IValidated{
    checkValidation();
}

interface Foo extends IMappable{}
interface Foo extends IValidated{}

@mappable
@validated
class Foo{
    save(){
        if (!this.checkValidation()){ return; }
    }
    load(){
        fetch('foo').then(resp => this.mapResponse(resp));
    }
}

Proposal:

Provide some mechanism of informing TypeScript that a decorated class is receiving new members from a decorator; in other words provide some mechanism to have the interface merging above happen implicitly.

This will merely allow TypeScript to recognize already-valid use cases with decorators. Again, the code above works fine, and I have many, many similar examples of that in production currently. This is just about making the type system more aware and able to deal with these use cases.

Needs Proposal Suggestion

Most helpful comment

Has there been any movement on this in the TypeScript team? I'm told decorators are almost stage 3, so I'd be curious and eager to hear if providing first-class support is on the radar at all.

All 14 comments

This would require some way of encoding side effects of the operation in the type system.

@DanielRosenwasser
I think that's true only for the mappable decorator. We could focus on the other case where the resulting type is equivalent to the return type of the decorator (the any return type needs to be deleted though) .

fwiw I added the any return type to make the TS compiler happy. Supporting both would be nice, but if I have to tuck instances of decorators that return subclasses in a js file and "lie" a little in a corresponding .d.ts file I'm happy to do it.

Just eager for any way to get decorators to play nicely with the type system.

It would sure be handy to be able to apply a class decorator that mutates (augments) the class and then be able to use those inside the class w/o additional boilerplate to tell the compiler they are there. It's easy to think of decorators at the same level as other class modifiers (like extends and implements).

This reminds me of extension methods , to draw a C# analogy. It's kind of invisible, but very handy.

vote +1

I'd like to offer an updated typings for class decorator:

// old
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
// new
declare type ClassDecorator = <
    TFunction extends Function,
    TDecoratedFunction extends TFunction
>(target: TFunction) => TDecoratedFunction | void;

that implies that a decorator can return another class as a result. With that, TS can properly understand what the shape of the class produced by the decorator.

In addition, I'd like to extend this proposal to member decorators as well, since they are very close. A member decorator should also be able to mutate type of the member:

// old
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
// new
declare type MethodDecorator = <T, R = T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<R> | void;

However, this can be a separate proposal.

Has there been any movement on this in the TypeScript team? I'm told decorators are almost stage 3, so I'd be curious and eager to hear if providing first-class support is on the radar at all.

I would love to see some progress on this too!
The whole idea of decorators is that you can abstract boilerplate away from users using your decorator.
If you have to tell consumers of your decorator "oh BTW, you need to add this 3-4 lines of type declaration to your class to be able to use my decorator without TS yelling at you" is kind of defeating the point.

From what I hear, TS has a policy against implementing ES features until they're stage 3. Decorators, I'm told, is almost to stage 3, so hopefully this'll finally get implemented sometime soon.

@arackaf Not really an issue for decorators, as they already have implemented those! There might be a breaking change if ES decorators will turn out quite different feature-wise. But there is not much that can be done about that anyway. That's the risk of implementing features before they are ready.

thing is people understand how they can and should use decorators.
Somehow those same people have a really hard time grasping higher-order functions. I don't know why that is, but that is just how it is. (nobody seems to realize those are virtually the same thing...)
I want to provide a couple of decorators that help them (and me!) to get rid of boilerplate.

TS is now hmm, not helpful in this area.

There might be a breaking change if ES decorators will turn out quite different feature-wise.

Yeah, they're completely different feature-wise :)

Instead of determining class prototype changes made by decorators magically, this could be solved by a native mixin support for most cases.

Example code could be looking like this:

class Mappable
{ public mapResponse() {} }

class Validated
{ public checkValidation(){} }

class Foo mixin Mappable, Validated
{
    save()
    {
        if (!this.checkValidation())
        { return; }
    }
    load()
    { fetch('foo').then(resp => this.mapResponse(resp)); }
}

Current workaround:

function mixin(...mixins: any[])
{
    return function (target: any)
    {
        for (let i = 0; i < mixins.length; ++i)
        {
            Object.getOwnPropertyNames(mixins[i].prototype).forEach(name =>
            { target.prototype[name] = mixins[i].prototype[name]; });
        }
    }
}

class Mappable
{ public mapResponse() {} }

class Validated
{ public checkValidation(){} }

interface Foo extends Mappable, Validated {}

@mixin(Mappable, Validated)
class Foo
{
    save()
    {
        if (!this.checkValidation())
        { return; }
    }
    load()
    { fetch('foo').then(resp => this.mapResponse(resp)); }
}

(Code not tested so please excuse any errors.)

Not a bad approach, IMO, except if I'm understanding right, it would not follow the standard decorator syntax. Also need to work out how to address configuration of/options passed into the mixin.

Do you mean the mixin keyword syntax? If so, it's not supposed to follow the decorator syntax, but rather be a language syntax extension looking similar to extends and inherits, because IMO it is a related operation. What do you mean with configuration and options passed into the mixin, though?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

manekinekko picture manekinekko  路  3Comments

blendsdk picture blendsdk  路  3Comments

Roam-Cooper picture Roam-Cooper  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments

dlaberge picture dlaberge  路  3Comments