Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

typescript

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

like image 582
Vivian River Avatar asked Aug 03 '16 22:08

Vivian River


People also ask

How do you use a class in TypeScript?

You can do this by using the new keyword, followed by a syntax similar to that of an arrow function, where the parameter list contains the parameters expected by the constructor and the return type is the class instance this constructor returns. The TypeScript compiler now will correctly compile your code.

Which keyword is used to define class in TypeScript?

TypeScript defines a constructor using the constructor keyword. A constructor is a function and hence can be parameterized. The this keyword refers to the current instance of the class. Here, the parameter name and the name of the class's field are the same.

What is new keyword in TypeScript?

The new keyword is used in javascript to create a object from a constructor function. The new keyword has to be placed before the constructor function call and will do the following things: Creates a new object. Sets the prototype of this object to the constructor function's prototype property.

What does the new keyword do when used with classes?

The Java new keyword is used to create an instance of the class. In other words, it instantiates a class by allocating memory for a new object and returning a reference to that memory. We can also use the new keyword to create the array object.


2 Answers

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:

  • https://github.com/rsp/ts-no-new

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

like image 123
rsp Avatar answered Oct 06 '22 15:10

rsp


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'

like image 42
Nitzan Tomer Avatar answered Oct 06 '22 14:10

Nitzan Tomer