Skip to content

Programatically Modifying Types #4490

Closed

Description

@kitsonk

I see a challenge currently in TypeScript in supporting alternative methods of OO programming. The topic has come up in several forms before, and there isn't a clear solution. Here is a summary of related tickets all in a similar problem space:

Currently, the only ways of "defining" types at design time:

  • Interfaces - they support extension and are open ended
  • Classes - supports traditional OO inheritance
  • Abstract Classes - like Interfaces, but called Abstract Classes 😉
  • Generics - Allows parameterisation of types
  • Unions - Allow concatenation of types
  • Intersections - More like a merge type actually 😉

In JavaScript, I can support other non-Inheritance and non-OO programming paradigms, but if I decide to extend those in TypeScript, I almost certainly will lose the static typing

For example, I can create a "composition/factory" in JavaScript, that makes it easy for me to "compose" functionality:

var compose = require('compose');

var factory = compose(function () { /* init */ }, {
    foo: 'bar'
}, {
    bar: 1
});

var a = factory.create();

But if I wanted all sorts of TypeScript goodness, it essentially starts to get really complicated. I could of sort of used Generics with Intersections, but the actual definition of the type and the implementation are decoupled and users of the compose library would have to learn some "arbitrary" syntax.

Now if there was a way, behind the scenes, to programatically merge/mixin types and modify them, I could abstract the user from the implementation and it would just work... I would love to be able to write something like this in TypeScript:

import compose from 'compose';

class Foo {
    foo: string = 'bar';
}

interface Bar {
    bar: number;
}

let factory = compose(function () { /* init */ }, 
    Foo,
    Bar
);

let a = factory.create();

Another example would be Aspect Oriented Programming. It is easy to support in JavaScript and actually the Decorators help a lot in TypeScript, but the challenge is that Decorators don't modify the type definition, and with after and around advice the return type can be modified. There is currently no way of expressing this in TypeScript.

There are probably loads of other patterns that are implemented in JavaScript which we cannot easily use in TypeScript and currently, it means us in the community are like a load of needy children, saying "I want, I want, I want, I need, I need, I need". Obviously a lot of this boils down to pseudo-religious debates, but without design time programmatic access to accessing and manipulating the types, we are going to constantly be running into these conflicts.

I am not sure of what the most logical and clean way the TypeScript team could implement something like this, but hopefully the team sees the need...

Activity

  1. weswigham commented on Aug 27, 2015

    @weswigham
    Member

    So... you can actually type your example fairly nicely in TS 1.6:

    interface MixinFactory<T> {
        create: () => T;
    }
    
    type AnyCtor<T> = (() => void) | (new() => T);
    
    function compose<A>(ctor: AnyCtor<A>, a: A): MixinFactory<A>;
    function compose<A, B>(ctor: AnyCtor<A&B>, a: A, b: B): MixinFactory<A&B>;
    function compose<A, B, C>(ctor: AnyCtor<A&B&C>, a: A, b:B, c:C): MixinFactory<A&B&C>;
    function compose<A, B, C, D>(ctor: AnyCtor<A&B&C&D>, a: A, b:B, c:C, d: D): MixinFactory<A&B&C&D>;
    function compose(ctor: new() => any, ...mixins: any[]): MixinFactory<any> {
        ctor.prototype = _.extend({}, ...mixins);
        return {
            create() {
                return new ctor();
            }
        };
    }

    The only caveat is the arbitrary number of overloads - there's no way to say that it could take an arbitrary number of type arguments and "merge them all" using current syntax.

    Similarly, there's no way to get the types of an array literal passed through the system. This literal:

    var myMixins = [{
        a: 2
    }, {
        b: 'twenty'
    }, {
        doTheThing() {
            console.log('The thing!');
        }
    }];

    Has the type ({a: number} | {b: string} | {doTheThing: () => void})[]. Strictly speaking, we could do better in this case if we remembered the arity of literals like this and associated each type with its slot in the tuple-like array. Considering we support tuple types in many places already, inferring this literal to be a tuple type may not be the worst idea. Giving it the type [{a: number}, {b: string}, {doTheThing: () => void}] seems way more accurate to me. If we could do that, then when I do

    var factory = compose(() => {}, ...myMixins);

    we know that the best overload is compose<A, B, C>, rather than the present result: the inability to find a matching overload. Unfortunately I've been informed that there's some other issues with array-vs-tuple type uncertainty which can make it difficult for us to do that (what if the array is edited? Then its type changes. How do we know it changes? Ich.).

    Regardless - you can still do this:

    var factory = compose(() => {
    
    }, {
        a: 2
    }, {
        b: 'twenty'
    }, {
        doTheThing() {
            console.log('The thing!');
        }
    });
    export var __instance = factory.create();
    export type MyClass = typeof __instance;
    export var create = factory.create;

    (I personally think that it's cute that you can extract the type in that way, though it would be better if typeof could resolve a bit more than namespaces on its own)

    And have full types flowing throughout your code.

  2. kitsonk commented on Aug 28, 2015

    @kitsonk
    ContributorAuthor

    Wesley Wigham (@weswigham) thanks! The overloading hadn't fully occurred to me. A couple of thoughts:

    The only caveat is the arbitrary number of overloads - there's no way to say that it could take an arbitrary number of type arguments and "merge them all" using current syntax.

    I think it has been discussed other places, but potentially a rest operator for generics is a part solution? Though how to operate it it programmatically would still need to be addressed unless it is implied a ...R generic is always a intersection. Something like this:

    function compose<...R>(ctor: AnyCtor<R>, /*...*/);

    Another thought, extracting the type via typeof is certainly useful. I think the challenge, I am seeing at the moment is while the extra "hoops" to jump through to extract the class information would be ok for some writing code, it become increasingly difficult for me to create a rational API I could reasonably get someone else to use (same to the challenges of the earlier of having to decide the arbitrary number of types).

    I mentioned Decorators above too, but I didn't give an example. Here is where I see things becoming difficult:

    interface AfterAdvice {
        (target: Object, result: any, args: any[]);
    }
    
    function after(advice: AfterAdvice) {
        return function(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> {
            let origFn = target[propertyKey];
            return {
                value: function () {
                    let result = origFn.call(target, arguments);
                    return advice.call(target, result, arguments);
                }
            }
        }
    }
    
    class A{
        @after(function () {
            return true;
        })
        foo(): string {
            return 'true';
        }
    }

    If I want a decorator to modify the type, I can't. I think the logic is the same for class decorators as well. It would be great if a decorator had some sort of ability to not only install functionality but change the type as well.

    I guess what I am asking for is that now is to allow operators to work on type aliases... for example:

    let type MyClass = typeof instance;
    MyClass.foo = typeof (a: string, b: number) => void;
    /* now MyClass type/interface has a property of foo which is a function */
    let myFactory = compose<MyClass>(/*...*/);

    Then I can do all sort of "magic" including handling the merging of type, intersections, conflict resolution, decoration and manipulation without it having to be supported "natively" in TypeScript...

  3. DanielRosenwasser commented on Aug 28, 2015

    @DanielRosenwasser
    Member

    Though how to operate it it programmatically would still need to be addressed unless it is implied a ...R generic is always a intersection.

    Sounds like you'd want $Either as well - check out #2710.

  4. kitsonk commented on Jun 13, 2017

    @kitsonk
    ContributorAuthor

    This really has been solved in lots of other ways. I don't have a legitimate use case anymore for this.

  5. locked and limited conversation to collaborators on Jun 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.SuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions