Vivian River
Vivian River

Reputation: 32400

In TypeScript, can a class be used without the "new" keyword?

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().

Upvotes: 11

Views: 20163

Answers (2)

rsp
rsp

Reputation: 111434

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

Upvotes: 7

Nitzan Tomer
Nitzan Tomer

Reputation: 164307

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'

Upvotes: 14

Related Questions