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()
.
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.
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.
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.
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.
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.
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.)
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 or
selfin web worker - see my module on npm [the-global-object](https://www.npmjs.com/package/the-global-object) for more info) and it will return
undefined`.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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)
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'
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With