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

Suggestion: callable class #183

Closed
fdecampredon opened this issue Jul 22, 2014 · 32 comments · Fixed by #32584
Closed

Suggestion: callable class #183

fdecampredon opened this issue Jul 22, 2014 · 32 comments · Fixed by #32584
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

@fdecampredon
Copy link

class MyCallableClass {
   property: string;
   constructor() {
     //constructor body
   }
   (): MyCallableClass { // could be 'string' or any other type
     // call signature body
     return new MyCallableClass();
   }
}

would emit :

function MyClass() {
  if (this instanceof MyClass) {
    //constructor body
  } else {
    // call signature body
    return new MyCallableClass();
  }
}
@fnky
Copy link

fnky commented Jul 22, 2014

+1 For this, as most libraries use this convention to omit the new operator

I had this in mind

static class Foo {
  constructor() {
    // constructor
  }
}

and the output would be

function Foo() {
  if(!(this instanceof Foo)) return new Foo();
  // constructor
}

@Igorbek
Copy link
Contributor

Igorbek commented Jul 22, 2014

Vote, could be useful.

@omidkrad
Copy link

Interesting ability, but I believe this can lead to bad design and confusing API. I like it how it is now, that I have to use the new keyword when I mean a new object created, or use methods when I mean a method call. In this case using named static methods.

@basarat
Copy link
Contributor

basarat commented Jul 28, 2014

For this, as most libraries use this convention to omit the new operator

and I hate those libraries. new all things

@RyanCavanaugh
Copy link
Member

+Needs Proposal (see https://github.com/Microsoft/TypeScript/wiki/Writing-Good-Design-Proposals)

This might be right out due to not aligning with ES6 class syntax. We'd rather not add any more to that than we have to.

A potential problem is that you would probably want to be able to call super() from a derived class method, but that would mean super() would mean two completely different things in constructors vs methods.

@RyanCavanaugh
Copy link
Member

Note: comments from Anders on #619

@rjamesnw
Copy link

rjamesnw commented Sep 8, 2014

A potential problem is that you would probably want to be able to call super() from a derived class method, but that would mean super() would mean two completely different things in constructors vs methods.

I'm not sure I agree. How that is handled is up to the developer. When I call "super()" I always expect to call a base function (always, as would normally happen in most JS implementations). There are ways to work around it, like moving the initialization to another method, or implementing the "instanceof" checks that some have mentioned. I don't see this changing at all. One of the main reasons for this is for object pools for game engines. If there's an available object in such an engine, I never call "super()" (which may create sub objects - and if I did need to, I should be able to just do "super.call(newObj, ....)"), but instead I would call 'oldObj.Reset()' (Construct 2 does something similar). If there isn't any new objects, then I'd simply return a "new ThisType()", which then would call "super()" as normal (as expected).

@jpolo
Copy link

jpolo commented Oct 31, 2014

+1 for this feature

For this, as most libraries use this convention to omit the new operator

The shortcut for a new object is not the best example actually. Usually in JS, applying 'call' on a constructor means the 'coerce' operator.

In my opinion this is a stronger usecase :

class MyCallableClass {
   property: string;
   constructor() { }
   (o: any): MyCallableClass {
     if (o instanceof MyCallableClass)  return o
     if ('toMyCallableClass' in o) return o.toMyCallableClass()
     return new MyCallableClass(o);//or throw or return null
   }
}

@saschanaz
Copy link
Contributor

Just a note: This is being discussed for ES7 - https://gist.github.com/ericelliott/1c6f451b2ed1b634c2f2#file-es7-new-fix-js

@Tragetaschen
Copy link

I'm currently missing something like this, but not particularly for a new replacement. I'm trying to implement a scale in d3, where the resulting object has

  • function call semantics: scale(3) scales the value 3 from the input domain to the output range
  • several functions like getting/setting the scale.domain() or scale.invert()ing the above

I haven't found any way to implement this interface in TypeScript without resorting to some hefty <any> casting.

@saschanaz
Copy link
Contributor

Just a note 2: ES7 Stage 1 proposal https://github.com/tc39/ecma262/blob/master/workingdocs/callconstructor.md

@mhegazy mhegazy added the ES7 Relates to the ES7 Spec label Jan 5, 2016
@mhegazy mhegazy added ES Next New featurers for ECMAScript (a.k.a. ESNext) and removed ES7 Relates to the ES7 Spec labels Feb 4, 2016
@qbolec
Copy link

qbolec commented May 25, 2016

I have a concrete problem with https://github.com/peerlibrary/meteor-reactive-field library which allows me to write code like this:

import { ReactiveField } from 'meteor/peerlibrary:reactive-field';
var  f = new ReactiveField(71); 
f(69); //set field to 69
console.log(f()); //retrieve 69

Im trying to write a declaration file for it, which would make complier happy.
I've tried:

declare module "meteor/peerlibrary:reactive-field" {
  export class ReactiveField<T> {
    (value?:T):T;
    constructor(value:T,equals?:(a:T,b:T)=>boolean);
  }
}

but it fails with error messages like:

client/imports/ui/recordPlayer/reusableRecordPlayer.ts (34, 7): Cannot invoke an expression whose type lacks a call signature.

whenever I try to call f().
I would appreciate any workaround which would allow me to declare this module.

@rjamesnw
Copy link

rjamesnw commented May 25, 2016

You could force cast it to a call signature. Something like var _f: { ():any } = <any>f; then call _f() instead wherever you need to. (For the first 'any', put the actual return type expected). You can also declare the interface instead of a class if that helps, which will allow the call signature. Start with export interface ... instead, and rename constructor( to new ( and give it a return type.

@DanielRosenwasser
Copy link
Member

@qbolec in the future, StackOverrflow is a better venue, but what we usually do is separate the instance/static sides into their own interfaces.

// Shape of the constructor function.
interface ReactiveFieldStatic {
    new <T>(value: T, equals: (a: T, b: T) => boolean): ReactiveField<T>;
}

// Shape of the instance.
interface ReactiveField<T> {
    (value?: T): T;
}

// The constructor function itself.
var ReactiveField: ReactiveFieldStatic;

@DanielRosenwasser
Copy link
Member

For the record, callable classes are now an inactive proposal. Check out portions of the discussion here.

@qbolec
Copy link

qbolec commented May 25, 2016

Thank you @DanielRosenwasser - it is very clever solution. I had my mind fixed on the bad assumption that "a var cannot be polymorphic", and totally failed to notice that it is not "var ReactiveField" that has to be polymorphic, but rather the "new" operator. Great idea!

@rjamesnw
Copy link

rjamesnw commented Apr 24, 2017

So the transcript of the discussion claims decorators can solve making classes callable. I'm looking into that and I fail to see how that could work - any examples?

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Apr 24, 2017

@rjamesnw With TypeScript's current implementation of decorators, I believe the following should work to make a class both new-able and callable:

function callable(ctor) {
    // Constructs 'ctor' when called *and* when constructed!
    return function (...args) {
        new ctor(...args);
    };
}

@callable
class C {
    constructor(...args) {
        console.log("Being constructed with: " + JSON.stringify(args));
    }
}

C(1, 2, 3, 4);
new C(5, 6, 7, 8);

Which gives

[log]: [1, 2, 3, 4]
[log]: [5, 6, 7, 8]

However, right now TypeScript can't communicate that both types are callable and constructable.

You can also differentiate the behavior by using new.target and giving the decorator a callback:

// Thing that gets passed a function and makes a decorator:
function callableWith(call: (...args: any[]) => any) {
    // Thing that actually decorates the class:
    return function decorator(construct: new (...args: any[]) => any) {
        // Thing that actually replaces the class:
        return function replacement (...args) {
            if (new.target) {
                return new construct(...args);
            }
            else {
                return call(...args);
            }
        }

    }
}

@callableWith((...args) => {
    console.warn("Hey, you probably want to use 'new C(...)' instead of 'C(...)'!")
    return new C(...args);
})
class C {
    constructor(...args) {
        console.log("Being constructed with: " + JSON.stringify(args));
    }
}

C(1, 2, 3, 4);
new C(5, 6, 7, 8);

which gives

[warn]: Hey, you probably want to use 'new C(...)' instead of 'C(...)'!
[log]: Being constructed with: [1,2,3,4]
[log]: Being constructed with: [5,6,7,8]

@rjamesnw
Copy link

rjamesnw commented Apr 25, 2017

Some points:

  1. Your code doesn't work at all in the playground nor VS 2017 nor VS Code. The "@callable" is underlined in red with the error "unable to resolve signature of class decorator when called as an expression". As a result, 'C' is not callable.
  2. I assume overloading is not supported this way, so there will only be one type of callable signature ... ? (hopefully "...args" is not the final calling signature, as without type safety I wouldn't see the point)
  3. for what I'm doing, I'm not sure that new.target is reliable, since not all browsers support it yet (and the TS polyfill is not that accurate).

Thanks.

Edit: For # 2 it appears VS 2017 is "stuck" at v2.1.5 (https://blogs.msdn.microsoft.com/typescript/2017/03/27/typescripts-new-release-cadence/). Perhaps as well, the Playground is also stuck at an older version, or the option is not enabled.

Side note: Regarding the main page: "TypeScript 2.2 is now available. Download our latest version today!"
You guys should really not be advertising V2.2 being out and present links to download VS 2015, 2017 and VS code, of which none of them have it.

@saschanaz
Copy link
Contributor

saschanaz commented Apr 25, 2017

@rjamesnw You can try VS2017.2 Preview which includes TS2.2 😄 https://www.visualstudio.com/en-us/news/releasenotes/vs2017-preview-relnotes

@rjamesnw
Copy link

rjamesnw commented Apr 25, 2017

Thanks, installed the preview. Turns out with v2.2 you can also use type intersections for this (was an issue in previous versions, as you couldn't represent both new and non-new signatures well across two different interfaces). At least now I can have an interface from a class, and merge it with a callable signature from another interface, and it will work now. The examples above however still don't work.

@rjamesnw
Copy link

rjamesnw commented Apr 29, 2017

@DanielRosenwasser For the record, new.target is always true due to the polyfill, unless targeting the newer ES versions, so that logic can easily break if not careful (or generate an exception error in strict mode, since the output is this.constructor).

@rjamesnw
Copy link

rjamesnw commented Apr 30, 2017

I mentioned this in another post, but thought I'd put it here also in case someone else comes here. ES6 introduces "specialized functions", and thus any direction towards a callable constructor is a bad idea. Targeting future ES versions will output using the "class" syntax, which will fail if called directly; For example, in Chrome, it generates the error Uncaught TypeError: Class constructor ... cannot be invoked without 'new'. In the end, there seems to be no point for TypeScript to support it and keep in line with the standards at the same time. It's too bad static methods are inherited (also as per spec); Just creates more complications over all when trying to use a common name on a type to overcome this issue.

@rjamesnw
Copy link

May I suggest instead, since callable constructors are not supported, that we can at least compromise with allowing the "new" modifier to allow forcing new static function signatures on derived types? Static functions could be used as factories (such as 'SomeType.create()') and accept different parameters based on type.

@athasach
Copy link

@DanielRosenwasser How might your decorator work for generic classes? For example, I have this case:

abstract class Try<T> {
}

class Success<T> extends Try<T> {
  constructor(public readonly value: T) {
    super();
  }
}

// How might I construct `Success<string>` without calling `new`?
const success = Success('successful');

@leonard-thieu
Copy link

Would it be possible to have call signatures just for ambient class declarations? It'd make it possible to accurately model some JavaScript libraries. Using two interfaces gets close but you lose type narrowing on instanceof. Plus, it'd be a whole lot cleaner.

@Domvel
Copy link

Domvel commented Dec 13, 2018

bump ... any news? :)

@DanielRosenwasser DanielRosenwasser added Out of Scope This idea sits outside of the TypeScript language design constraints Revisit An issue worth coming back to Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript Out of Scope This idea sits outside of the TypeScript language design constraints Revisit An issue worth coming back to labels Dec 13, 2018
@DanielRosenwasser
Copy link
Member

Nope!

@rjamesnw
Copy link

rjamesnw commented Dec 13, 2018

It's not possible to have callable classes. It is not supported in the spec. Calling a class will cause errors (try it yourself). For this reason it makes sense that this will never be supported. That said, I could agree however that if we could force-enable functions (instead of the new class syntax) and still be able to export ES6+ code, or detect such cases and auto-export functions instead, that callable classes in this case would of course work. Anyhow, good luck with that. LOL ;)

> var c = class Test { };
> c();
VM183:1 Uncaught TypeError: Class constructor Test cannot be invoked without 'new'
    at <anonymous>:1:1

@Domvel
Copy link

Domvel commented Mar 11, 2019

After a few month I facing this issue again.
But now I think: Should I really support callable classes? Don't hit the 👎 yet. 😄 Let me explain my thinking. 🤔

A class should be instantiated and not called as function. If you call the class which checks this is an instance of class-constructor and returns any (or what you want) if it called as function is a bit "confusing" and a bit against the strict typing in TypeScript.

Hold on ... I still 👍 this issue. But I just thinking loud.
You can export a function which just do the job as suggested here.
e.g.

export class UUID {
  value: string;
  constructor() {
    this.generate();
  }
  // ...
}

export const newUUID = (): string => {
  return new UUID().toString();
};

const uuidInstance = new UUID();
const uuidString = newUUID();

vs

function UUID() {
  if (this instanceof UUID) {
    // Called as class.
    this.generate();
  } else {
    // Called as function.
    return new UUID().toString();
  }
}

It's a great 'feature' in JavaScript. But maybe not the right for TypeScript.
I like JavaScript, I love TypeScript and with VS Code I would like to intimate. 😄

NOTE: How should this work in ES6 and "real" classes? (class keyword)
If you compile TypeScript to ES6 with class-feature. You'll get an TypeError:
Class constructor UUID cannot be invoked without 'new' ...

@goodmind
Copy link

goodmind commented May 1, 2019

Agree, just enable it for ambient classes, Flow supports this

@jpolo
Copy link

jpolo commented May 1, 2019

@Domvel This issue is about expressiveness of the language. The Constructor() is not a simple shortcut to the constructor it is often used as the coercion operator (cf #183 (comment)).

A good way to measure the expressiveness of Typescript is that it should allow to reimplement all ECMA spec just in pure TS (= no definitions). At the moment it is quite hard to express in Typescript a class that would behave like String, Date, Number, etc because of the lack of callable classes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
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

Successfully merging a pull request may close this issue.