Class as a value

Topics: General
Jan 12, 2014 at 3:59 PM
Hello,

I'd like to know if it's possible (or will be possible) to treat classes as regular variables. What I need is to create a factory function that creates classes that close over the outer function's arguments.

Contrived example:
function makeClass(fun: (a: number) => void) {
    class Cls { // error here - statement expected
        calc(a: number) {
            return fun(a);
        }
    }
    return Cls;
}
I could use regular JavaScript functions but I'd lose the types and class syntactic sugar features.
Jan 13, 2014 at 11:09 AM
I'd just declare the public interface of the class externally, something like:
declare class Cls {
     calc(a: number);
}

function makeClass(fun: (a: number) => void) : typeof Cls {
    
    return <any>(
        function Cls(){
            this.calc = fun;    
        }
    );
}

var Type = makeClass((a:number)=>alert(a));

var instance = new Type();
instance.calc(5);

Jan 13, 2014 at 11:32 AM
That's a nice workaround to get the types to match.

Sadly inside the makeClass function you still can't use full class syntax sugar (for example for instance methods) and I'd have to write prototype functions and the rest of regular JavaScript like that:
declare class Cls {
     calc(a: number);
}

function makeClass(fun: (a: number) => void) : typeof Cls {
    function Cls(){
    }

    Cls.prototype.calc = (a: number) => fun(a);
    
    return <any>Cls;
}

var Type = makeClass((a:number)=>alert(a));

var instance = new Type();
instance.calc(5);
The real class that I want to use it is not that simple and turning it into JS inside TS would make it a lot less readable.

But thanks for the suggestion!
Developer
Jan 13, 2014 at 1:54 PM
There are no plans to support classes nested within functions in 1.0, but since it is a planned feature of ES6 it is certainly something we'll look at post-1.0. BTW, an interesting aspect of this feature is that you wouldn't be able to reference the class outside the containing function, so you wouldn't have a name for the type of objects returned by the function unless you also declare an interface outside.
Jan 13, 2014 at 2:36 PM
Thanks for the explanation!

Wouldn't (in my example) Type contain the reference to the nested class (if nested classes were implemented)?
var Type = makeClass((a:number)=>alert(a));

// later
export = Type;
The Cls inside the function would be returned and assigned to Type and Type would be of the same shape as Cls (meaning typeof Cls).

Or do you mean that I wouldn't be able to reference the type of the nested class in makeClass function declaration (typeof Cls wouldn't be valid as Cls is nested)?
Developer
Jan 13, 2014 at 4:46 PM
What I mean is that you wouldn't be able to reference the class as a type. In the example above, Type would be a variable that references the constructor function of the class. You'd be able to call it to produce class instances, but you wouldn't have a name for the type of those instances--unless you declare a compatible interface outside the 'makeClass' function.
var Type = makeClass((a: number) => alert(a));
var x: ??? = new Type();   // No denotable name for type of x
Since the type system is structural this isn't a problem per se. You can just declare an interface with the appropriate shape and you're fine. It's just something to note.
Jan 13, 2014 at 6:42 PM
Oh, now I get it. Or at least I think I do :)

Variables do not introduce types like class statements. Class statements (class X { ... }) introduce type (for the static type analysis) as well as variable with the name.

Thanks again!
Jan 13, 2014 at 7:34 PM
The real class that I want to use it is not that simple and turning it into JS inside TS would make it a lot less readable.
_

Cloning Cls is an alternative in that case:
class Cls {
     foo = "foo";
     calc(a: number){}
     bar(){ 
         alert(this.foo);
     }
}

function makeClass(fun: (a: number) => void) : typeof Cls {
    
    function ClsClone(){
        this.foo = "foo";
    }
    
    for(var prop in Cls.prototype){
        ClsClone.prototype[prop] = Cls.prototype[prop];
    }

    ClsClone.prototype.calc = (a: number) => fun(a);
    
    return <any>ClsClone;
}
Jan 13, 2014 at 7:59 PM
Well, that's interesting! Why didn't I think about this?!

Personally I'd create a subclass instead of clone (code below) but I like your idea as it preserves 99% of TypeScript sugar with 1% of "native JavaScript shim".

Excellent, thanks nabog!
class Cls {
     foo = "foo";
     calc(a: number) { }
     bar(){ 
         alert(this.foo);
     }
}

function makeClass(fun: (a: number) => void) : typeof Cls {
    
    function ClsClone() {
        Cls.apply(this, arguments); // calls super constructor, this takes care of foo = "foo"
    }
    
    ClsClone.prototype = Object.create(Cls.prototype); // instead of copying everything we just setup prototype chain

    ClsClone.prototype.calc = (a: number) => fun(a);
    
    return <any>ClsClone;
}