typescript: 'new' call to function
Asked Answered
P

5

5

Recently I thought of converting a side project of mine into Typescript. But I have trouble with calling functions using new.

I am trying to call a function imported from another file like this:

// Function in 'file.js'
function Foo() {
  this.x = 1;
  this.y = 2;
}
Foo.prototype.set = function() {
   return this.x + this.y;
};
export { Foo };
// Function in another file calling Foo
import { Foo } from './file';
function getSum() {
  let foo = new Foo(); // I got the below error here!!!
  foo.set();
}

When I try to type this, I get this error: 'new' expression, whose target lacks a construct signature, implicitly has an 'any' type..


Looking at the typescript documentation, I understand the call signature should be written as follows:

type SomeConstructor = {
  new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
  return new ctor("hello");
}

But I don't know how to apply the above type to my 'Foo' function. I tried to apply the construct signature to the function but couldn't place it right.

// Function in 'file.js' --> renamed to 'file.tsx'
type FooType = {
  x: number,
  y: number,
};

type FooConstructor = {
  new (): FooType
};

function Foo(this: FooType) { // How do I add FooConstructor to this?
  this.x = 1;
  this.y = 2;
}
Foo.prototype.set = function(): number {
   return this.x + this.y;
};

I couldn't apply it while exporting / importing or during function call either. All of the below throw errors.

export { Foo: FooConstructor };
import { Foo: FooConstructor } from './file';
let foo = new Foo() as FooConstructor;

So should I have to change the Foo function into a class, is that the only possible way to type it?! I see many blogs showing how to type a class. But even with that approach, I got an error saying, Type 'FooType' is not assignable to type 'FooConstructor'.

And I am lost here. Any help is appreciated!


EDIT: My File.ts looks like this now:

I add the declaration in the File.ts file, like this:

type FooType = {
  x: number,
  y: number,
};

declare class Foo {
  constructor();
  x: number;
  y: number;
  setHeader(): number;
}

function Foo(this: FooType) {
  this.x = 1;
  this.y = 2;
}
Foo.prototype.set = function(): number {
   return this.x + this.y;
};

export { Foo };
Pantograph answered 7/3, 2021 at 17:21 Comment(3)
Credit a correspondingly named declaration file, in this case named file.d.ts, alongside file.js with the following content export declare class Foo { constructor(x: number, y: number); set(): number; }Triploid
Tweaking @AluanHaddad's declaration a bit because it seems like you want a zero-argument constructor and public properties x and y: declare class Foo { constructor(); set(): number; x: number; y: number; } tsplay.dev/WJ98lmContorted
I tried adding the above declaration in the file file.d.ts. But I still get 'new' expression, whose target lacks a construct signature, implicitly has an 'any' type. in the file where I need to call Foo!Pantograph
P
6

The only way to solve this situation was to convert the below function to a class:

function Foo(this: FooType) { // How do I add FooConstructor to this?
  this.x = 1;
  this.y = 2;
}
Foo.prototype.set = function(): number {
   return this.x + this.y;
};

to:

class Foo() {
  x: number;
  y: number;
  constructor() {
    this.x = 1;
    this.y = 2;
  }
  set (): number {
   return this.x + this.y;
  }
}
Pantograph answered 16/5, 2021 at 17:51 Comment(0)
A
2

Officially, Typescript decided not to offer syntax support to this kind of valid "old school" JavaScript. They prefered the class syntax.

However, it's possible to make it work, copied from a previous answer of mine:

// Only instance members here.
export interface Circle { // Name the interface with the same than the var.
  radius: number;
  area: () => number;
  perimeter: () => number;
}

// You could define static members here.
interface CircleConstructor {
  new(radius: number): Circle;
}

export const Circle = function(this: Circle, radius: number) {
  const pi = 3.14;
  this.radius = radius;
  this.area = function () {
    return pi * radius * radius
  }
  this.perimeter = function () {
    return 2 * pi * radius;
  }
} as unknown /*as any*/ as CircleConstructor; // Note the trust-me casting
    
const c = new Circle(3); // okay

This comes with some issues. For instance, the fact that it's using any type which is forbidden or the necessary use of the as operator which is mega-ugly. As improvement, I've used unknown instead of any but the core problem remains. This makes the lint tool to not complain, so it's a big improvement over the any.

This is the best solution I've found so far. Typescript devs are aware of this, but they have decided to not provide support for this kind of "syntax".

Anarthrous answered 17/12, 2022 at 20:33 Comment(2)
Could you please explain/show how to implement static methods in this example? I know where to define them in the interface, but not where to implement them.Universalize
You can simply implement the body of a static method by assigning it to a property of the class-function object. E.g. Circle.myStaticMethod = () => {console.log();}; right above the const c = new Circle(3); in the example. Just like you would do in pure JavaScript.Chekiang
H
1

If you have legacy JavaScript code that needs to be typed as TypeScript, you can create definition files that contain only types (the runtime code remains in JavaScript).

From the original example, a file.js file containing a Foo class written in ES5 could have a sibling file.d.ts containing this:

// file.d.ts
export declare class Foo {
  x: number;
  y: number;
  constructor();
  set(): number;
}

// Any other TypeScript file
import { Foo } from 'any/path/file';
let foo = new Foo(); // Foo
const num = foo.set(); // number

So the mistake in the question was to move the legacy JavaScript code to a TypeScript file (TypeScript does not support ES5 as the source code, at least for classes) instead of writing a definition file.

Note: in this specific example, the constructor(); is superfluous because it's the implicit default constructor, but you could type the constructor with any number of arguments, of any type, and even leverage function overloads to define multiple signatures.

TypeScript playground

As a more general answer to "how to type a newable function which is not written as an ES6 class?", it is also possible to use the new keyword in any TypeScript type or interface:

interface Foo {
  x: number;
  y: number;
  set(): number;
}

type Constructor<T> = new (...args: any[]) => T;

declare const newableFunction: Constructor<Foo>;

const obj = new newableFunction(); // Foo

TypeScript playground

In this case again, if the implementation is a plain old function, it has to be written preferably in a separate JavaScript file, otherwise the compiler will complain about the type of the declaration not matching with the type of the implementation.

To go further, consider this kind of hybrid function, which can be called with or without the new keyword:

// Function in 'file.js'
function newableFunction() {

  const self = Object.getPrototypeOf(this) === newableFunction.prototype ? this : {};

  self.x = 1;
  self.y = 2;
  self.get = function () {
    return self.x + self.y;
  }
  return self;
}

This special function could be typed like this:

interface Foo {
  x: number;
  y: number;
  get(): number;
}

interface Constructor<T> {
  (...args: any[]): T
  new (...args: any[]): T
}

export const newableFunction: Constructor<Foo>;

Example of usage:

import { newableFunction } from './file';

const foo1 = newableFunction(); // Foo
const foo2 = new newableFunction(); // Foo

This kind of construct can also be found in lib.es5.d.ts native JavaScript types, for example the Number constructor:

interface NumberConstructor {
    new(value?: any): Number;
    (value?: any): number;
    // ...
Hockett answered 14/12, 2022 at 18:14 Comment(0)
S
0

In .d.ts file or directly in .ts file:

declare class Foo {
    public x: number;
    public y: number;

    set(): number;
}

Playground

Saguenay answered 7/3, 2021 at 21:30 Comment(22)
The answer here needs a constructor as @Aluan Haddad had commented above!Pantograph
I don't think that it needs a constructor. Already the above declaration implies an implicit constructor without arguments. Hover Foo after new in the Playground and you will see that. It might however be good to add a constructor to make it clear that the constructor doesn't take any arguments.Saguenay
@AmithRaravi No I wouldn't include a constructor. The only reason I did so originally was because I miss read the question and thought it took parameters. The only thing missing in this answer is the export keywordTriploid
Okay, so I tried to run it by adding export & removing the constructor. But it throws this error: TypeError: Duplicate declaration "Foo" now. It throws this error even with constructor included..Pantograph
Maybe the TS engine parses the file where you implement function Foo()? Could you show your file structure?Saguenay
@AmithRaravi that is a runtime error. In a declaration file, .d.ts, nothing that you write has any runtime implications. You must be doing something wrong.Triploid
@Saguenay & Aluan Haddad I have updated the File.tx structure in the question above! As adding the structure in the comment doesn't format it..Pantograph
Put the declare class Foo code in a .d.ts file or directly in the file where you use Foo, not in the file where Foo is implemented.Saguenay
@AmithRaravi. This code works for me.Saguenay
@Saguenay the code in that repo is completely screwed up. You have a source file in your output directory and you haven't placed the declaration file alongside the source file.Triploid
@AluanHaddad. The declaration file is located alongside the TS source file. Why is it a problem that I put the compiled TS code in the same directory as the JS source file that I use?Saguenay
Because it's a terrible practice. You should be able to build from scratch, fully removing the output directory beforehand, without issue.Triploid
I can accept that it's bad practice to mix source and output in one directory for the reason you give. It doesn't affect whether the code works, however. But where the declaration file is put depends on the case. If I publish a JS library and want to help TS users, I put declarations alongside my JS code. If I use a JS library that lacks TS declarations, I write declarations myself and put them alongside my TS code.Saguenay
@Saguenay Hmmmm, importing only the type declaration (without actually importing from the File.ts) leads to a Runtime error, because the function is not found!Pantograph
@AmithRaravi. Of course it does. When running you have to import the implementation.Saguenay
@Saguenay Okay then, I am back to totally confused again. Because importing the implementation (while having the declaration in the file directly or in a File.d.ts file) throws TypeError: Duplicate declaration "File" !Pantograph
@AmithRaravi. What is File? That's not a name you have used above.Saguenay
@Saguenay sorry for the confusion, I meant Foo. TypeError: Duplicate declaration "Foo"Pantograph
@AmithRaravi. Can you share what your code looks like now? Perhaps a simplified and "anonymized" example? But it must give that error.Saguenay
@Saguenay sorry for the delay, please find the minimal example here. Run yarn start to see the error I get. Thank you in advance!!Pantograph
@AmithRaravi. Now I understand your problem and have tested with an even more minimal example without being able to find a solution.Saguenay
@Saguenay Before building Create React App based example in the comment above, I tried to reproduce this in a vanilla HTML/Javascript website. But I couldn't, the code works with Typescript / Browserify bundling! Since I had the problem originally in a React project, I am still searching for a solution to the problem. Thank you for spending so much time on this though!!Pantograph
H
0

If you need to apply the same example like this one, you can do the following - I will try to be more generic with this example:

Given you have this piece of code:

class Parent {
  constructor(name: string) {
    console.log(`Name is: ${name}`)
  }
  parent_method() { 
    console.log('Method in the parent class')
  }
}
class ChildOne extends Parent {
  constructor(name: string) {
    super(name)
  }
  child_one_method() {
    console.log('Child method in ChildOne class')
  }
}
class ChildTwo extends Parent {
  constructor(name: string) {
    super(name)
  }
  child_two_method() {
    console.log('Child method in ChildTwo class')
  }
}

type SomeConstructor<T extends Parent> = new (s: string)=> T

This way, you can have a function that initiate an instance for a generic type:

function initializer<T extends Parent>(ctor: SomeConstructor<T>, name: string) {
  return new ctor(name)
}

And notice instead of using the hard-coded parameter in the example given in the doc, I have rewritten the generic initializer for you:

function fn_1(ctor: SomeConstructor<ChildOne>) {
  return new ctor("Child One");
}

Now comes the question, how to call initializer method?? Here is how to create an instance of your choice and giving the paameter value as well:

const child_one = initializer(ChildOne, 'CHILD ONE')  // "Name is: CHILD ONE"
const child_two = initializer(ChildTwo, 'CHILD TWO')  // "Name is: CHILD TWO"

child_one.child_one_method()  // "Child method in ChildOne class"
child_two.child_two_method()  // "Child method in ChildTwo class" 
child_one.parent_method()  // "Method in the parent class" 

Please Notice this behavior looks like the Reflection in Java where you can get an instance of any Generic Type with a given parameter.


If you still need to have the same implementation like that given in the Docs, use this method instead of initializer:

function fn_2(ctor: SomeConstructor<ChildTwo>) {
  return new ctor("Child Two");
}

And to use this method:

const two = fn_2(ChildTwo)  // "Name is: Child Two"
two.child_two_method()   //  "Child method in ChildTwo class" 

Notice This example is not as generic as the one mentioned above.

Finally, here is working example in the Playground

Homopterous answered 27/2, 2023 at 14:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.