Skip to content

Strongly typing a JS functional-style "class" #2299

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

Closed
nycdotnet opened this issue Mar 11, 2015 · 6 comments
Closed

Strongly typing a JS functional-style "class" #2299

nycdotnet opened this issue Mar 11, 2015 · 6 comments
Labels
By Design Deprecated - use "Working as Intended" or "Design Limitation" instead

Comments

@nycdotnet
Copy link

I'm trying to convert a JavaScript functional-style "class" from JS to strongly-typed TS making minimal changes to the code other than adding type annotations. I do not want to use the class keyword (this is an experiment to ease a refactoring scenario, not newly-written code). I also want the code to compile correctly using --noImplicitAny.

The following code works as expected and compiles cleanly (in 1.4), however I am "cheating" by using the ClassFunction type which is an alias for any.

interface FunctionalPoint {
    new (x: number, y: number) : FunctionalPoint;
    x: number;
    y: number;
    isOrigin(): boolean;
}

type ClassFunction = any;

var FunctionalPoint: FunctionalPoint = <ClassFunction>function (x: number, y: number) {
    var self: FunctionalPoint = this;
    self.x = x;
    self.y = y;
    self.isOrigin = () => {
        return self.x === 0 && self.y === 0;
    }
};

var point1 = new FunctionalPoint(1, 1);
var point2 = new FunctionalPoint(0, 0);

var result = `point 1 is origin: ${point1.isOrigin() }, point 2 is origin: ${point2.isOrigin() }.`;

alert(result);

If the type assertion of <ClassFunction> on the function is removed, I get this error:

Type '(x: number, y: number) => void' is not assignable to type 'FunctionalPoint'.
  Property 'x' is missing in type '(x: number, y: number) => void'.

Is there any way to do this without resorting to using any or refactoring to use classes? I read through the spec and 4.11 seems to indicate that if there is an apparent construct signature (which I believe the interface FunctionalPoint has), then "the result type of the function call becomes the result type of the operation" - which I believe should be FunctionalPoint, no?

Please forgive me if I am being dense on this. Thanks very much.

@danquirk
Copy link
Member

Why do you need ClassFunction instead of the cast to any? An explicit cast still satisfies your requirement of compiling without errors under noImplicitAny.

@nycdotnet
Copy link
Author

Thanks, Dan. Of course, using <any> and <ClassFunction> are the same in terms of the type system. Using <ClassFunction> has the practical benefit of letting me easily identify where I've done this particular hack via "Find all references" . Is there any way to do this without the cast to any (or type aliases of any)?

@danquirk
Copy link
Member

Ah, gotcha.

First, you probably intend to have a different shape for your class here. An interface with a construct signature generally should only have static members in it as it would be describing the constructor function. Like with your code there you can now legally do new point1(1,2) which is probably not what you intended. Instead you want:

interface FunctionalPoint {
    x: number;
    y: number;
    isOrigin(): boolean;
}

interface FunctionalPointConstructor {
    new (x: number, y: number) : FunctionalPoint;
}

var FunctionalPoint = <FunctionalPointConstructor><Function>function (x: number, y: number) { ... }

That said, there's still not really a better solution here than what you've done. The compiler only allows new on functions that return void and then new will return an any which will trigger your implicit any warnings. You want to cast the function to FunctionalPointConstructor but we don't allow that assignment, requiring a cast to Function or any (or some alias of those as you did).

It's possible we should add a new exception to the assignability rules specifically for this case (assigning void returning functions to construct signatures), as we essentially already have this special case of how void returning functions are newable to handle this pattern but don't allow you to follow through to stronger typing.

@nycdotnet
Copy link
Author

Hi Dan - thanks very much for this excellent explanation. I hadn't considered that side-effect (regarding being able to call new on an instance), and I should have thought to cast down to <Function> instead of <any>.

I do think that it would be nice to be able to close the loop in the type system in this manner. I was following the style used by examples on the Knockout JS tutorial site for this experiment. For example, ReservationsViewModel on this page: http://learn.knockoutjs.com/#/?tutorial=collections

Because these are samples from a popular library, I expect there is a lot of JS out there that looks just like this and it would be helpful to have a way to completely type it without needing to do unnecessary refactoring to proper TS classes or needing to do a non-obvious double-cast. Plus this would be a nice bone for those who are simply against classes and prefer the JS functional style (though most of those folks don't care for new either...)

Thanks.

@danquirk
Copy link
Member

Yep, makes sense to me. I logged a new issue to capture that language change suggestion in a more concise form. I'll just close this one as by design and it can serve as an additional reference for that one.

@danquirk danquirk added the By Design Deprecated - use "Working as Intended" or "Design Limitation" instead label Mar 12, 2015
@nycdotnet
Copy link
Author

Much appreciated.

@microsoft microsoft locked and limited conversation to collaborators Jun 18, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
By Design Deprecated - use "Working as Intended" or "Design Limitation" instead
Projects
None yet
Development

No branches or pull requests

2 participants