11

TypeScript is a superset of ES6 Javascript that includes types. A class may be declared using the class keyword and instantiated using the new keyword similarly to how they are in Java.

I was wondering if there is any use case in TypeScript where a class may be instantiated without using the new keyword.

The reason I ask is because I was wonder if, suppose I have a class called Bob, can I assume that any instance of Bob is instantiated with new Bob().

Vivian River
  • 31,198
  • 62
  • 198
  • 313

2 Answers2

14

Typescript safeguards against this by default, so if you do this:

class A {}
let a = A();

You'll get an error:

Value of type typeof A is not callable. Did you mean to include 'new'?

However there are some objects that can be created without using the new keyword, basically all native types.
If you look at the lib.d.ts you can see the signatures of the different constructors, for example:

StringConstructor:

interface StringConstructor {
    new (value?: any): String;
    (value?: any): string;
    ...
}

ArrayConstructor:

interface ArrayConstructor {
    new (arrayLength?: number): any[];
    new <T>(arrayLength: number): T[];
    new <T>(...items: T[]): T[];
    (arrayLength?: number): any[];
    <T>(arrayLength: number): T[];
    <T>(...items: T[]): T[];
    ...
}

As you can see there are always the same ctors with and without the new keyword.
You can of course imitate this behavior if you wish.

What's important to understand is that while typescript checks to make sure that this doesn't happen, javascript doesn't check, and so if someone writes js code that will use your code he might forget to use new, so this situation is still a possibility.

It's quite easy to detect if this happens at runtime and then handle it as you see fit (throw an error, fix it by returning an instance using new and log it).
Here's a post that talks about it: Creating instances without new (plain js), but the tl;dr is:

class A {
    constructor() {
        if (!(this instanceof A)) {
            // throw new Error("A was instantiated without using the 'new' keyword");
            // console.log("A was instantiated without using the 'new' keyword");

            return new A();
        }
    }
}

let a1 = new A(); // A {}
let a2 = (A as any)(); // A {}

(code in playground)


Edit

As far as I know, it's not possible to make the compiler understand that A can be called without the new keyword without casting it.
We can do a bit better than cast it to any:

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

let a2 = (A as AConstructor)(); // A {}

The reason that we cannot do the trick that is being done for (i.e.) the Array in lib.d.ts:

interface Array<T> {
    ...
}

interface ArrayConstructor {
    ...
}

declare const Array: ArrayConstructor;

Is that here they use Array once as a type and once as a value, but a class is both a type and a value, so trying to do this trick will end with:

Duplicate identifier 'A'

Nitzan Tomer
  • 155,636
  • 47
  • 315
  • 299
  • 3
    Sounds like you're saying that the short answer is "yes". – Vivian River Aug 04 '16 at 16:40
  • indeed, The short answer is: yes, it's possible to call a ctor without using `new` (but in such a case you won't have an instance) – Nitzan Tomer Aug 04 '16 at 17:51
  • Now the **real** question is: how to make it work with `A()` instead of `(A as any)()`? – rsp Jan 28 '19 at 15:48
  • @rsp check my revised answer – Nitzan Tomer Jan 29 '19 at 11:19
  • Thanks for your update. You can see [**my answer**](https://stackoverflow.com/questions/38754854/in-typescript-can-a-class-be-used-without-the-new-keyword/54455994#54455994) with some solutions that I came up with when I was approaching the problem some time ago. Maybe you'll have some ideas to improve them. – rsp Jan 31 '19 at 08:14
  • I tried to run your examples and they only work if you transpile to ES5 or older. If you transpile to ES6 or newer you get `TypeError: Class constructor A cannot be invoked without 'new'` (transpiling to legacy JS adds a lot of runtime cost for certain features like `async` / `await` and since you care about it then you should consider targetting the latest ES version that you can - you can configure it as `"target": "es2017"` in tsconfig.json) – rsp Jan 31 '19 at 13:16
  • @rsp yeah, that's true. when the compiled js used classes then the runtime environment won't let it happen – Nitzan Tomer Jan 31 '19 at 13:35
  • Well, the bit about how this is done for `Object` and friends sure seems like it could be quite useful in typings for JavaScript libraries ... – SamB May 13 '19 at 18:28
7

This is quite tricky but can be done in many ways, depending on how closely you want it to resemble the behavior of e.g. the built-in Array constructor that works like that.

Problem #1 - constructor cannot be called without 'new'

This is not specific to TypeScript, this is a problem of JavaScript.

Constructors created with the class keyword cannot be called without new in JavaScript:

> class A {}
undefined
> new A()
A {}
> A()
TypeError: Class constructor A cannot be invoked without 'new'

Just like arrow functions cannot be called with new:

> B = () => {}
[Function: B]
> B()
undefined
> new B()
TypeError: B is not a constructor

Only functions created with the function keyword can be called both with and without new:

> function C() {}
undefined
> C()
undefined
> new C()
C {}

(What is funny is that if you transpile both arrow functions and class keyword constructors to JS older than ES6 then all A(), B() and C() above will work both with and without new as they all will get transpiled to old style functions with the function keyword and work just fine even on current engines.)

Problem #2 - constructor doesn't get the right 'this' without 'new'

Once you overcome the problem of errors invoking your constructor, you need to make sure that the constructor actually gets a new object.

In JavaScript (and in TypeScript) the new keyword creates a new object and binds it to this in the constructor, and returns it. So if you have a function:

function f() {
  console.log(this);
}

then if you call it as new f() it will print and empty object and return it.

If you call it as f() without new then it will print the global object (window in browsers or global in Node orselfin web worker - see my module on npm [the-global-object](https://www.npmjs.com/package/the-global-object) for more info) and it will returnundefined`.

Problem #3 - static types are tricky to define

This problem is TypeScript-specific. You need to make sure that all the types work as expected and they work in a useful way. It's easy to declare everything as any but then you'll loose all of the hints in your editor and the TypeScript compiler will not detect type errors during compilation.

Problem #4 - it's easy to make a solution that doesn't work the same

This problem is again not specific to TypeScript but general to JavaScript. You want everything to work as expected - inheritance using both old-style functions and explicit prototypes and inheritance with class and extends keywords to work plus a lot more.

In other words the object should work the same as other objects declared with class and instantiated with new with no fancy stuff.

My rule of thumb: if you can do something with built-ins like Array (that work with and without new) then you should do it with our constructor as well.

Problem #5 - it's easy to make a solution with different meta data

Again general to JavaScript. What you want is not only to get an object that works like you want when you call A() without new but you actually want to x instanceof A to work as expected, you want console.log() to write the correct name when you want to print the object etc.

This may not be a problem for everyone but needs to be considered.

Problem #6 - it's easy to make a solution with old-school function

It should support the class syntax instead of going back to function constructors and prototypes or otherwise you'll lose a lot of useful TypeScript features.

Problem #7 - some solutions work only when transpiled to ES5 or older

This is related to Problem #6 above - if the transpilation target is pre-ES6 then the result will use old-style function constructors which don't give the error:

TypeError: Class constructor A cannot be invoked without 'new'

(see Problem #1 above)

This may or may not be a problem for you. If you are transpiling for legacy engines anyway then you won't see this problem but when you change the transpilation target (e.g. to avoid high runtime cost of async/await polyfills etc.) then you'r code will break. If it's a library code then it will not work for everyone. If it's only for your own use then at least keep it in mind.

Solutions

Here are some of the solutions that I came up with when I was thinking about that some time ago. I am not 100% happy with them, I would like to avoid proxies, but those are currently the only solutions that I found that solve all of the problems above.

Solution #1

One of my first attempts (for more general types see later examples):

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;
    }
  }
);

One disadvantage of that is that while the constructor name is ok and it prints fine, the constructor property itself points to the original constructor that was an argument to the nonew() function instead of to what the function returns (which may or may not be a problem, depending on how you loot at it).

Another disadvantage is the need to declare interfaces to have the types exposed.

Solution #2

Another solution:

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' });

Here you don't need to duplicate the type definitions in redundant interfaces but instead you get the original constructor with the $ prefix. Here you also get inheritance and instanceof working and the constructor name and printing is ok but the constructor property points to the $-prefixed constructors.

Solution #3

Another way to do it:

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' });

This solution has the constructor properties of instances point to the exposed (not the $-prefixed constructor) but makes the constructor property return true for hasOwnProperty() - but false for propertyIsEnumerable() so that should not be a problem.

More solutions

I put all of my attempts and some more explanation on GitHub:

I am not completely happy with any one of them but they all work in what they do.

See also my answer to Call constructor on TypeScript class without new

rsp
  • 107,747
  • 29
  • 201
  • 177
  • IMO all of these solutions are extreme overkills. I see nothing wrong with have a `AContrcutor` type and then just do `const a = (A asAContrcutor)()`. All of your solutions try to avoid that but end up being way too verbose and add not only too much ts code, but also end up in more redundant runtime (js) code. – Nitzan Tomer Jan 31 '19 at 09:17
  • @NitzanTomer it all depends on what is your goal here. If you want to make `new` optional to type less then `a = (A asAContrcutor)()` is certainly not less to type than `a = new A()`. If you want to make a constructor that is a drop-in replacement for `Array` then it needs to work as both `a = new A()` and `a = A()` with no additional syntax. My use case is for a library code where I want my users to have convenient syntax even if it's more code for me. Also note that I managed to use proxies only for the constructors so the actual usage of the objects has no runtime cost at all. – rsp Jan 31 '19 at 12:36
  • @NitzanTomer I understood in the question asking for no `new` keyword that it was implicit that other keywords should not be introduced as well. Otherwise we could more easily add a static factory function to each class called e.g. `create` and use `a = A.create()` instead of `a = new A()` but I think that `a = new A()` without `new` should actually be `a = A()` and nothing more. Maybe that's not what the OP had in mind but so far there's no accepted answer so we don't know yet. My own requirements are described on github (in answer) and if you have an idea to do it with no proxies let me know – rsp Jan 31 '19 at 12:41
  • All of your code adds js code that isn't really needed. You over complicate things for something that isn't very important to begin with. If you offer an sdk then just hide the class and only expose a factory function and then you won't need all this complexity. You want to avoid complication, both in your code (so that other developers will be able to understand it easily) and to remove unneeded runtime code. – Nitzan Tomer Jan 31 '19 at 12:45
  • @NitzanTomer maybe for you it is not important but for me it is. I want a drop-in replacement for Array and other built-in classes. For that `a = A()` must be equivalent to `a = new A()` the `instanceof` must work the same etc. The `a = A()` being equivalent to `a = new A()` is a hard requirement. If you have an idea how to simplify it then I am more than happy to listen to your idea as I wrote on https://github.com/rsp/ts-no-new but to saying me that having a drop-in Array replacement "isn't very important to begin with" is disappointing. I solved it, if you have a better solution let me know – rsp Jan 31 '19 at 12:52
  • @NitzanTomer about the complexity - the solution is basically: `const nonew = c => new Proxy(X, { apply(t, i, a) { return new t(...a); } });` and everything else is typing and making sure that the returned meta data is correct but if you don't care about meta data or you don't mind having `any` types then it's very simple. As for the added runtime cost I wouldn't complain without benchmarking. – rsp Jan 31 '19 at 12:59
  • and a few more variables to make this meta data thing work. my policy is not to add js code to pacify the ts compiler, which is what you're doing. my solution works great with `instanceof. The only problem with my solution is inheritance. If you need that, then my solution really isn't good. – Nitzan Tomer Jan 31 '19 at 13:23
  • @NitzanTomer Don't get me wrong, I don't think your solution is bad (in fact I up voted your answer before I wrote mine), just my requirements (inheritance, built-in-like behavior and no dependence on transpilation target) made me wrote a different solutions long time ago so I thought I'll share them here in case someone finds it useful. But as you can see in my repo I am not happy with what I came up with (which is messy, I agree) but that was the best I could do back then that does the job and I hope someone will improve it. But without requirements like mine I would recommend your solution. – rsp Jan 31 '19 at 15:27
  • With the need for inheritance and es6 classes, my solution doesn't work, that's true. It's a shame that there's no support for it, but IMO all of these workarounds make it too complex. I'd really look for other solutions, and also I think it's fine to require the need for the `new` keyword. I don't know what's your use case so I can't really say, if you need to make it work then I can't think of better solutions. – Nitzan Tomer Jan 31 '19 at 20:48