Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Programatically Modifying Types #4490

Closed
kitsonk opened this issue Aug 27, 2015 · 4 comments
Closed

Programatically Modifying Types #4490

kitsonk opened this issue Aug 27, 2015 · 4 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@kitsonk
Copy link
Contributor

kitsonk commented Aug 27, 2015

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...

@weswigham
Copy link
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.

@DanielRosenwasser DanielRosenwasser added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Aug 27, 2015
@kitsonk
Copy link
Contributor Author

kitsonk commented Aug 28, 2015

@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...

@DanielRosenwasser
Copy link
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.

@kitsonk
Copy link
Contributor Author

kitsonk commented Jun 13, 2017

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

@kitsonk kitsonk closed this as completed Jun 13, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants