Skip to content

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

Closed
@nycdotnet

Description

@nycdotnet

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.

Activity

danquirk

danquirk commented on Mar 11, 2015

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

nycdotnet commented on Mar 11, 2015

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

danquirk commented on Mar 11, 2015

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

nycdotnet commented on Mar 12, 2015

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

danquirk commented on Mar 12, 2015

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

added
By DesignDeprecated - use "Working as Intended" or "Design Limitation" instead
on Mar 12, 2015
nycdotnet

nycdotnet commented on Mar 12, 2015

@nycdotnet
Author

Much appreciated.

locked and limited conversation to collaborators on Jun 18, 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

    By DesignDeprecated - use "Working as Intended" or "Design Limitation" instead

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @nycdotnet@danquirk

        Issue actions

          Strongly typing a JS functional-style "class" · Issue #2299 · microsoft/TypeScript