Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Call constructor on TypeScript class without new

In JavaScript, I can define a constructor function which can be called with or without new:

function MyClass(val) {
    if (!(this instanceof MyClass)) {
        return new MyClass(val);
    }

    this.val = val;
}

I can then construct MyClass objects using either of the following statements:

var a = new MyClass(5);
var b = MyClass(5);

I've tried to achieve a similar result using the TypeScript class below:

class MyClass {
    val: number;

    constructor(val: number) {
        if (!(this instanceof MyClass)) {
            return new MyClass(val);
        }

        this.val = val;
    }
}

But calling MyClass(5) gives me the error Value of type 'typeof MyClass' is not callable. Did you mean to include 'new'?

Is there any way I can make this pattern work in TypeScript?

like image 878
dan Avatar asked Sep 27 '15 10:09

dan


People also ask

Can constructor be called without new?

No, this is not possible. Constructors that are created using the class keyword can only be constructed with new , if they are [[call]]ed without they always throw a TypeError 1 (and there's not even a way to detect this from the outside).

How do you call a constructor from a class in TypeScript?

In TypeScript, the constructor method is always defined with the name "constructor". In the above example, the Employee class includes a constructor with the parameters empcode and name . In the constructor, members of the class can be accessed using this keyword e.g. this. empCode or this.name .

How do you use constructors in TypeScript?

Constructors are identified with the keyword "constructor". A Constructor is a special type of method of a class and it will be automatically invoked when an instance of the class is created. A class may contain at least one constructor declaration.

Can you manually call a constructor?

You can call a constructor manually with placement new.


6 Answers

What about this? Describe the desired shape of MyClass and its constructor:

interface MyClass {
  val: number;
}

interface MyClassConstructor {
  new(val: number): MyClass;  // newable
  (val: number): MyClass; // callable
}

Notice that MyClassConstructor is defined as both callable as a function and newable as a constructor. Then implement it:

const MyClass: MyClassConstructor = function(this: MyClass | void, val: number) {
  if (!(this instanceof MyClass)) {
    return new MyClass(val);
  } else {
    this!.val = val;
  }
} as MyClassConstructor;

The above works, although there are a few small wrinkles. Wrinkle one: the implementation returns MyClass | undefined, and the compiler doesn't realize that the MyClass return value corresponds to the callable function and the undefined value corresponds to the newable constructor... so it complains. Hence the as MyClassConstructor at the end. Wrinkle two: the this parameter does not currently narrow via control flow analysis, so we have to assert that this is not void when setting its val property, even though at that point we know it can't be void. So we have to use the non-null assertion operator !.

Anyway, you can verify that these work:

var a = new MyClass(5); // MyClass
var b = MyClass(5); // also MyClass

Hope that helps; good luck!


UPDATE

Caveat: as mentioned in @Paleo's answer, if your target is ES2015 or later, using class in your source will output class in your compiled JavaScript, and those require new() according to the spec. I've seen errors like TypeError: Class constructors cannot be invoked without 'new'. It is quite possible that some JavaScript engines ignore the spec and will happily accept function-style calls also. If you don't care about these caveats (e.g., your target is explicitly ES5 or you know you're going to run in one of those non-spec-compliant environments), then you definitely can force TypeScript to go along with that:

class _MyClass {
  val: number;

  constructor(val: number) {
    if (!(this instanceof MyClass)) {
      return new MyClass(val);
    }

    this.val = val;
  }
}
type MyClass = _MyClass;
const MyClass = _MyClass as typeof _MyClass & ((val: number) => MyClass)

var a = new MyClass(5); // MyClass
var b = MyClass(5); // also MyClass

In this case you've renamed MyClass out of the way to _MyClass, and defined MyClass to be both a type (the same as _MyClass) and a value (the same as the _MyClass constructor, but whose type is asserted to also be callable like a function.) This works at compile-time, as seen above. Whether your runtime is happy with it is subject to the caveats above. Personally I'd stick to the function style in my original answer since I know those are both callable and newable in es2015 and later.

Good luck again!


UPDATE 2

If you're just looking for a way of declaring the type of your bindNew() function from this answer, which takes a spec-conforming class and produces something which is both newable and callable like a function, you can do something like this:

function bindNew<C extends { new(): T }, T>(Class: C & {new (): T}): C & (() => T);
function bindNew<C extends { new(a: A): T }, A, T>(Class: C & { new(a: A): T }): C & ((a: A) => T);
function bindNew<C extends { new(a: A, b: B): T }, A, B, T>(Class: C & { new(a: A, b: B): T }): C & ((a: A, b: B) => T);
function bindNew<C extends { new(a: A, b: B, d: D): T }, A, B, D, T>(Class: C & {new (a: A, b: B, d: D): T}): C & ((a: A, b: B, d: D) => T);
function bindNew(Class: any) {
  // your implementation goes here
}

This has the effect of correctly typing this:

class _MyClass {
  val: number;

  constructor(val: number) {    
    this.val = val;
  }
}
type MyClass = _MyClass;
const MyClass = bindNew(_MyClass); 
// MyClass's type is inferred as typeof _MyClass & ((a: number)=> _MyClass)

var a = new MyClass(5); // MyClass
var b = MyClass(5); // also MyClass

But beware the the overloaded declarations for bindNew() don't work for every possible case. Specifically it works for constructors which take up to three required parameters. Constructors with optional paramaters or multiple overload signatures will probably not be properly inferred. So you might have to tweak the typings depending on use case.

Okay, hope that helps. Good luck a third time.


UPDATE 3, AUG 2018

TypeScript 3.0 introduced tuples in rest and spread positions, allowing us to easily deal with functions of an arbitrary number and type of arguments, without the above overloads and restrictions. Here's the new declaration of bindNew():

declare function bindNew<C extends { new(...args: A): T }, A extends any[], T>(
  Class: C & { new(...args: A): T }
): C & ((...args: A) => T);
like image 67
jcalz Avatar answered Oct 02 '22 06:10

jcalz


The keyword new is required for ES6 classes:

However, you can only invoke a class via new, not via a function call (Sect. 9.2.2 in the spec) [source]

like image 45
Paleo Avatar answered Oct 02 '22 07:10

Paleo


Solution with instanceof and extends working

The problem with most of the solution I've seen to use x = X() instead of x = new X() are:

  1. x instanceof X doesn't work
  2. class Y extends X { } doesn't work
  3. console.log(x) prints some other type than X
  4. sometimes additionally x = X() works but x = new X() doesn't
  5. sometimes it doesn't work at all when targeting modern platforms (ES6)

My solutions

TL;DR - Basic usage

Using the code below (also on GitHub - see: ts-no-new) you can write:

interface A {
  x: number;
  a(): number;
}
const A = nn(
  class A implements A {
    x: number;
    constructor() {
      this.x = 0;
    }
    a() {
      return this.x += 1;
    }
  }
);

or:

class $A {
  x: number;
  constructor() {
    this.x = 10;
  }
  a() {
    return this.x += 1;
  }
}
type A = $A;
const A = nn($A);

instead of the usual:

class A {
  x: number;
  constructor() {
    this.x = 0;
  }
  a() {
    return this.x += 1;
  }
} 

to be able to use either a = new A() or a = A() with working instanceof, extends, proper inheritance and support for modern compilation targets (some solutions only work when transpiled to ES5 or older because they rely on class translated to function which have different calling semantics).

Full examples

#1

type cA = () => A;

function nonew<X extends Function>(c: X): AI {
  return (new Proxy(c, {
    apply: (t, _, a) => new (<any>t)(...a)
  }) as any as AI);
}

interface A {
  x: number;
  a(): number;
}

const A = nonew(
  class A implements A {
    x: number;
    constructor() {
      this.x = 0;
    }
    a() {
      return this.x += 1;
    }
  }
);

interface AI {
  new (): A;
  (): A;
}

const B = nonew(
  class B extends A {
    a() {
      return this.x += 2;
    }
  }
);

#2

type NC<X> = { new (): X };
type FC<X> = { (): X };
type MC<X> = NC<X> & FC<X>;
function nn<X>(C: NC<X>): MC<X> {
  return new Proxy(C, {
    apply: (t, _, a) => new (<any>t)(...a)
  }) as MC<X>;
}

class $A {
  x: number;
  constructor() {
    this.x = 0;
  }
  a() {
    return this.x += 1;
  }
}
type A = $A;
const A: MC<A> = nn($A);
Object.defineProperty(A, 'name', { value: 'A' });

class $B extends $A {
  a() {
    return this.x += 2;
  }
}
type B = $B;
const B: MC<B> = nn($B);
Object.defineProperty(B, 'name', { value: 'B' });

#3

type NC<X> = { new (): X };
type FC<X> = { (): X };
type MC<X> = NC<X> & FC<X>;
function nn<X>(C: NC<X>): MC<X> {
  return new Proxy(C, {
    apply: (t, _, a) => new (<any>t)(...a)
  }) as MC<X>;
}

type $c = { $c: Function };

class $A {
  static $c = A;
  x: number;
  constructor() {
    this.x = 10;
    Object.defineProperty(this, 'constructor', { value: (this.constructor as any as $c).$c || this.constructor });
  }
  a() {
    return this.x += 1;
  }
}
type A = $A;
var A: MC<A> = nn($A);
$A.$c = A;
Object.defineProperty(A, 'name', { value: 'A' });

class $B extends $A {
  static $c = B;
  a() {
    return this.x += 2;
  }
}
type B = $B;
var B: MC<B> = nn($B);
$B.$c = B;
Object.defineProperty(B, 'name', { value: 'B' });

#2 simplified

type NC<X> = { new (): X };
type FC<X> = { (): X };
type MC<X> = NC<X> & FC<X>;
function nn<X>(C: NC<X>): MC<X> {
  return new Proxy(C, {
    apply: (t, _, a) => new (<any>t)(...a)
  }) as MC<X>;
}

class $A {
  x: number;
  constructor() {
    this.x = 0;
  }
  a() {
    return this.x += 1;
  }
}
type A = $A;
const A: MC<A> = nn($A);

class $B extends $A {
  a() {
    return this.x += 2;
  }
}
type B = $B;
const B: MC<B> = nn($B);

#3 simplified

type NC<X> = { new (): X };
type FC<X> = { (): X };
type MC<X> = NC<X> & FC<X>;
function nn<X>(C: NC<X>): MC<X> {
  return new Proxy(C, {
    apply: (t, _, a) => new (<any>t)(...a)
  }) as MC<X>;
}

class $A {
  x: number;
  constructor() {
    this.x = 10;
  }
  a() {
    return this.x += 1;
  }
}
type A = $A;
var A: MC<A> = nn($A);

class $B extends $A {
  a() {
    return this.x += 2;
  }
}
type B = $B;
var B: MC<B> = nn($B);

In #1 and #2:

  • instanceof works
  • extends works
  • console.log prints correctly
  • constructor property of instances point to the real constructor

In #3:

  • instanceof works
  • extends works
  • console.log prints correctly
  • constructor property of instances point to the exposed wrapper (which may be an advantage or disadvantage depending on the circumstances)

The simplified versions don't provide all meta-data for introspection if you don't need it.

See also

  • My answer to: In TypeScript, can a class be used without the "new" keyword?
  • My GitHub repo with more examples: https://github.com/rsp/ts-no-new
like image 34
rsp Avatar answered Oct 02 '22 07:10

rsp


My workaround with a type and a function:

class _Point {
    public readonly x: number;
    public readonly y: number;

    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}
export type Point = _Point;
export function Point(x: number, y: number): Point {
    return new _Point(x, y);
}

or with an interface:

export interface Point {
    readonly x: number;
    readonly y: number;
}

class _PointImpl implements Point {
    public readonly x: number;
    public readonly y: number;

    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

export function Point(x: number, y: number): Point {
    return new _PointImpl(x, y);
}

like image 20
keos Avatar answered Oct 02 '22 08:10

keos


TL;DR

If you are targeting ES6 and you really want to use class to store your data, not a function:

  • Create a function that simply invokes your class constructor with its arguments;
  • Set that function's prototype to the prototype of your class.

From now you are able to call that function either with or without new keyword to generate new class instances.

Typescript playground


Typescript provides an ability to create such a function (let's call it a "callable constructor") in a strongly typed way. Well, any type is necessary in intermediate type definitions (replacing it with unknown causes errors), but this fact will not affect your experience.

First of all we need to define basic types to describe entities we are working with:

// Let's assume "class X {}". X itself (it has type "typeof X") can be called with "new" keyword,
// thus "typeof X" extends this type
type Constructor = new(...args: Array<any>) => any;

// Extracts argument types from class constructor
type ConstructorArgs<TConstructor extends Constructor> =
    TConstructor extends new(...args: infer TArgs) => any ? TArgs : never;

// Extracts class instance type from class constructor
type ConstructorClass<TConstructor extends Constructor> =
    TConstructor extends new(...args: Array<any>) => infer TClass ? TClass : never;

// This is what we want: to be able to create new class instances
// either with or without "new" keyword
type CallableConstructor<TConstructor extends Constructor> =
  TConstructor & ((...args: ConstructorArgs<TConstructor>) => ConstructorClass<TConstructor>);

The next step is to write a function that accepts regular class constructors and creates corresponding "callable constructors".

function CreateCallableConstructor<TConstructor extends Constructor>(
    type: TConstructor
): CallableConstructor<TConstructor> {
    function createInstance(
        ...args: ConstructorArgs<TConstructor>
    ): ConstructorClass<TConstructor> {
        return new type(...args);
    }

    createInstance.prototype = type.prototype;
    return createInstance as CallableConstructor<TConstructor>;
}

Now all we have to do is to create our "callable constructor" and check it really works.

class TestClass {
  constructor(readonly property: number) { }
}

const CallableTestConstructor = CreateCallableConstructor(TestClass);

const viaCall = CallableTestConstructor(56) // inferred type is TestClass
console.log(viaCall instanceof TestClass) // true
console.log(viaCall.property) // 56

const viaNew = new CallableTestConstructor(123) // inferred type is TestClass
console.log(viaNew instanceof TestClass) // true
console.log(viaNew.property) // 123

CallableTestConstructor('wrong_arg'); // error
new CallableTestConstructor('wrong_arg'); // error
like image 28
N. Kudryavtsev Avatar answered Oct 02 '22 07:10

N. Kudryavtsev


I like @N. Kudryavtsev solution for creation smart instance factories (constructor wrapping with CreateCallableConstructor). But simple Reflect.construct(type, args) works perfectly if using any[] args is enough. Here is the example with mobx (v5), which shows that there is no problems with prototypes and decorators:


import { observable, reaction } from "mobx";

class TestClass {
  @observable
  stringProp: string;

  numProp: number;

  constructor(data: Partial) {
    if (data) {
      Object.assign(this, data);
    }
  }
}

var obj = Reflect.construct(TestClass, [{numProp: 123, stringProp: "foo"}]) as TestClass;
// var obj = new TestClass({numProp: 123, stringProp: "foo"});

console.log(JSON.stringify(obj));

reaction(() => obj.stringProp, v => {
    console.log(v);
  }
);

obj.stringProp = "bar";

And even this simple wrapper functions works:


type Constructor = new (...args: any[]) => any;
const createInstance = (c: Constructor, ...args) => new c(...args);
var obj = createInstance(TestClass, {numProp: 123, stringProp: "foo"});

// or
const createInstance1 = (c: Constructor) => (...args) => new c(...args);
var obj1 = createInstance(TestClass)({numProp: 123, stringProp: "foo"}, 'bla');

like image 26
SalientBrain Avatar answered Oct 02 '22 07:10

SalientBrain