TypeScript:对函数使用'new'关键字调用

5

最近我考虑将我的一个副业项目转换成Typescript。但是我在使用new关键字调用函数时遇到了麻烦。

我正在尝试像这样调用从另一个文件导入的函数:

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

当我尝试输入时,出现了以下错误:'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.


查看TypeScript文档后,我理解应该按以下方式编写调用签名:

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

但我不知道如何将上述类型应用于我的“Foo”函数。 我尝试将构造函数签名应用于该函数,但无法正确放置。

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

在导入/导出或函数调用时,我无法应用它。以下所有情况都会抛出错误。

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

那我是不是必须将Foo函数改写为一个类,这是唯一的方法吗?!我看到很多博客介绍了如何编写一个类。但即便采用那种方法,我还是会收到一个错误信息:Type 'FooType' is not assignable to type 'FooConstructor'

我感到有些迷惑。任何帮助都将不胜感激!


编辑:我的File.ts文件现在看起来像这样:

我在File.ts文件中添加了声明,就像这样:

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

3
请在文件名为file.js的文件旁边附加一个同名的声明文件file.d.ts,其内容如下:export declare class Foo { constructor(x: number, y: number); set(): number; } - Aluan Haddad
1
稍微调整一下@AluanHaddad的声明,因为似乎你想要一个零参数构造函数和公共属性xydeclare class Foo { constructor(); set(): number; x: number; y: number; } https://tsplay.dev/WJ98lm - Linda Paiste
我尝试在文件 file.d.ts 中添加上述声明。但是在需要调用 Foo 的文件中仍然出现了 'new' expression, whose target lacks a construct signature, implicitly has an 'any' type. 的错误提示。 - Amith Raravi
5个回答

6
唯一解决此情况的方法是将以下函数转换为类:
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;
  }
}

2

官方上来讲,Typescript决定不支持这种有效的“老派”JavaScript语法。他们更喜欢使用class语法。

不过,通过从我以前的一个答案中复制,可以使其工作:

// 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

这带来了一些问题。例如,它使用了被禁止的any类型或必要时使用的丑陋的as运算符。为了改进,我已经使用了unknown代替any,但核心问题仍然存在。这使得lint工具不会抱怨,因此相比于any是一个很大的改进。
这是目前我找到的最佳解决方案。TypeScript开发人员知道这个问题,但他们已经决定不支持这种“语法”。

请问您能否解释/展示如何在这个例子中实现静态方法?我知道在接口中定义它们的位置,但不知道在哪里实现它们。 - florian norbert bepunkt
1
你可以通过将静态方法赋值给类函数对象的属性来简单实现静态方法的主体。例如,在示例中的const c = new Circle(3);之前,使用Circle.myStaticMethod = () => {console.log();};。就像在纯JavaScript中一样。 - José Cabo

1
如果您有需要作为TypeScript类型的遗留JavaScript代码,您可以创建仅包含类型的定义文件(运行时代码仍然是JavaScript)。
从原始示例中,包含ES5编写的Foo类的file.js文件可以有一个兄弟文件file.d.ts,其中包含以下内容:
// 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

因此,问题在于将遗留的 JavaScript 代码移动到 TypeScript 文件中(TypeScript 不支持 ES5 作为源代码,至少对于类而言),而不是编写定义文件。
注意:在这个特定的例子中,constructor(); 是多余的,因为它是隐式默认构造函数,但你可以使用任意数量、任何类型的参数来类型化构造函数,甚至利用function overloads来定义多个签名。

TypeScript playground

对于“如何为非ES6类编写的可实例化函数进行类型定义”的更一般性回答,也可以在任何TypeScript的typeinterface中使用new关键字:

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

在这种情况下,如果实现是一个普通的函数,最好将其写在一个单独的JavaScript文件中,否则编译器会抱怨声明的类型与实现的类型不匹配。

进一步考虑这种混合函数,可以使用或不使用new关键字调用:

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

这个特殊功能可以这样输入:
interface Foo {
  x: number;
  y: number;
  get(): number;
}

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

export const newableFunction: Constructor<Foo>;

使用示例:
import { newableFunction } from './file';

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

这种结构也可以在 lib.es5.d.ts 的原生 JavaScript 类型中找到,例如 Number 构造函数:
interface NumberConstructor {
    new(value?: any): Number;
    (value?: any): number;
    // ...

0

在 .d.ts 文件或直接在 .ts 文件中:

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

    set(): number;
}

游乐场


这里的答案需要一个构造函数,就像@Aluan Haddad在上面评论中提到的那样! - Amith Raravi
2
我认为它不需要构造函数。以上声明已经暗示了一个无需参数的隐式构造函数。在 Playground 中使用 new 后悬停在 Foo 上,你会看到这一点。但是,添加一个构造函数以明确构造函数不需要任何参数可能是有益的。 - md2perpe
2
@AmithRaravi 不,我不会包括构造函数。我最初这样做的唯一原因是因为我误读了问题,认为它需要参数。这个答案唯一缺少的就是 export 关键字。 - Aluan Haddad
好的,我尝试通过添加导出并删除构造函数来运行它。但是现在它抛出了这个错误:TypeError: 重复声明 "Foo"。即使包括构造函数,它仍然会抛出此错误。 - Amith Raravi
可能是 TS 引擎解析了你实现 function Foo() 的文件?你能展示一下你的文件结构吗? - md2perpe
显示剩余17条评论

0
如果您需要应用与this one相同的示例,可以执行以下操作 - 我将尝试使用此示例更加通用:
假设您有这段代码:
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

这样,您可以编写一个函数来初始化一个通用类型的实例:

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

请注意,与文档中给出的硬编码参数不同,我已经为您重新编写了通用的初始化程序:

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

现在出现了一个问题,如何调用initializer方法?下面是创建一个实例并传递参数值的方法:
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" 

请注意,这种行为类似于Java中的Reflection,您可以使用给定的参数获取任何通用类型的实例。

如果您仍需要与文档中给出的相同实现,请使用此方法而不是initializer

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

而要使用这个方法:

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

注意:此示例不如上述示例通用。

最后,在Playground中,这里有一个可用的示例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接