在没有 new 的情况下调用 TypeScript 类的构造函数

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/32807163/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-21 03:06:30  来源:igfitidea点击:

Call constructor on TypeScript class without new

constructortypescript

提问by dan

In JavaScript, I can define a constructor function which can be called with or without new:

在 JavaScript 中,我可以定义一个构造函数,可以使用或不使用它来调用new

function MyClass(val) {
    if (!(this instanceof MyClass)) {
        return new MyClass(val);
    }

    this.val = val;
}

I can then construct MyClassobjects using either of the following statements:

然后我可以MyClass使用以下任一语句构造对象:

var a = new MyClass(5);
var b = MyClass(5);

I've tried to achieve a similar result using the TypeScript class below:

我尝试使用下面的 TypeScript 类获得类似的结果:

class MyClass {
    val: number;

    constructor(val: number) {
        if (!(this instanceof MyClass)) {
            return new MyClass(val);
        }

        this.val = val;
    }
}

But calling MyClass(5)gives me the error Value of type 'typeof MyClass' is not callable. Did you mean to include 'new'?

但是打电话MyClass(5)给了我错误Value of type 'typeof MyClass' is not callable. Did you mean to include 'new'?

Is there any way I can make this pattern work in TypeScript?

有什么办法可以让这个模式在 TypeScript 中工作吗?

回答by jcalz

What about this? Describe the desired shape of MyClassand its constructor:

那这个呢?描述所需的形状MyClass及其构造函数:

interface MyClass {
  val: number;
}

interface MyClassConstructor {
  new(val: number): MyClass;  // newable
  (val: number): MyClass; // callable
}

Notice that MyClassConstructoris defined as both callable as a function and newable as a constructor. Then implement it:

请注意,它MyClassConstructor被定义为可调用的函数和可更新的构造函数。然后实现它:

const MyClass: MyClassConstructor = function(this: MyClass | void, val: number) {
  if (!(this instanceof MyClass)) {
    return new MyClass(val);
  } else {
    this!.val = val;
  }
} as MyClassConstructor;

The above works, although there are a few small wrinkles. Wrinkle one: the implementation returns MyClass | undefined, and the compiler doesn't realize that the MyClassreturn value corresponds to the callable function and the undefinedvalue corresponds to the newable constructor... so it complains. Hence the as MyClassConstructorat the end. Wrinkle two: the thisparameter does not currently narrow via control flow analysis, so we have to assert that thisis not voidwhen setting its valproperty, even though at that point we know it can't be void. So we have to use the non-null assertion operator !.

以上工作,虽然有一些小皱纹。皱纹一:实现返回MyClass | undefined,编译器没有意识到MyClass返回值对应于可调用函数,undefined值对应于newable构造函数......所以它会抱怨。因此as MyClassConstructor在最后。问题二:this参数当前没有通过控制流分析缩小范围,所以我们必须在设置它的属性时断言它this不是,即使那时我们知道它不能是。所以我们必须使用非空断言运算符voidvalvoid!

Anyway, you can verify that these work:

无论如何,您可以验证这些工作:

var a = new MyClass(5); // MyClass
var b = MyClass(5); // also MyClass

Hope that helps; good luck!

希望有所帮助;祝你好运!



UPDATE

更新

Caveat: as mentioned in @Paleo's answer, if your target is ES2015 or later, using classin your source will output classin your compiled JavaScript, and those requirenew()according to the spec. I've seen errors like TypeError: Class constructors cannot be invoked without 'new'. It is quite possible that some JavaScript engines ignore the spec and will happily accept function-style calls also. If you don't care about these caveats (e.g., your target is explicitly ES5 or you know you're going to run in one of those non-spec-compliant environments), then you definitely can force TypeScript to go along with that:

警告:如@Paleo的回答中所述,如果您的目标是 ES2015 或更高版本,则class在您的源代码中使用将class在您编译的 JavaScript 中输出,而那些需要new()根据规范。我见过类似的错误TypeError: Class constructors cannot be invoked without 'new'。一些 JavaScript 引擎很可能会忽略规范,并且很乐意接受函数式调用。如果你不关心这些警告(例如,你的目标明确是 ES5 或者你知道你将在那些不符合规范的环境之一中运行),那么你绝对可以强制 TypeScript 遵循这一点:

class _MyClass {
  val: number;

  constructor(val: number) {
    if (!(this instanceof MyClass)) {
      return new MyClass(val);
    }

    this.val = val;
  }
}
type MyClass = _MyClass;
const MyClass = _MyClass as typeof _MyClass & ((val: number) => MyClass)

var a = new MyClass(5); // MyClass
var b = MyClass(5); // also MyClass

In this case you've renamed MyClassout of the way to _MyClass, and defined MyClassto be both a type (the same as _MyClass) and a value (the same as the _MyClassconstructor, but whose type is asserted to also be callable like a function.) This works at compile-time, as seen above. Whether your runtime is happy with it is subject to the caveats above. Personally I'd stick to the function style in my original answer since I know those are both callable and newable in es2015 and later.

在这种情况下,你已经改名MyClass了的方式_MyClass,并定义MyClass为既是类型(同_MyClass)和值(一样的_MyClass构造,但其类型被肯定,也可调用的功能等)。这在编译时工作,如上所示。您的运行时是否满意取决于上述注意事项。就我个人而言,我会坚持原始答案中的函数风格,因为我知道这些在 es2015 及更高版本中都是可调用和可更新的。

Good luck again!

再次祝你好运!



UPDATE 2

更新 2

If you're just looking for a way of declaring the type of your bindNew()function from this answer, which takes a spec-conforming classand produces something which is both newable and callable like a function, you can do something like this:

如果您只是想bindNew()这个答案中寻找一种声明函数类型的方法,它需要符合规范class并产生像函数一样既可更新又可调用的东西,您可以执行以下操作:

function bindNew<C extends { new(): T }, T>(Class: C & {new (): T}): C & (() => T);
function bindNew<C extends { new(a: A): T }, A, T>(Class: C & { new(a: A): T }): C & ((a: A) => T);
function bindNew<C extends { new(a: A, b: B): T }, A, B, T>(Class: C & { new(a: A, b: B): T }): C & ((a: A, b: B) => T);
function bindNew<C extends { new(a: A, b: B, d: D): T }, A, B, D, T>(Class: C & {new (a: A, b: B, d: D): T}): C & ((a: A, b: B, d: D) => T);
function bindNew(Class: any) {
  // your implementation goes here
}

This has the effect of correctly typing this:

这具有正确键入以下内容的效果:

class _MyClass {
  val: number;

  constructor(val: number) {    
    this.val = val;
  }
}
type MyClass = _MyClass;
const MyClass = bindNew(_MyClass); 
// MyClass's type is inferred as typeof _MyClass & ((a: number)=> _MyClass)

var a = new MyClass(5); // MyClass
var b = MyClass(5); // also MyClass

But beware the the overloaded declarations for bindNew()don't work for every possible case. Specifically it works for constructors which take up to three required parameters. Constructors with optional paramaters or multiple overload signatures will probably not be properly inferred. So you might have to tweak the typings depending on use case.

但要注意重载的声明bindNew()不适用于所有可能的情况。具体来说,它适用于最多需要三个必需参数的构造函数。可能无法正确推断具有可选参数或多个重载签名的构造函数。因此,您可能需要根据用例调整类型。

Okay, hope thathelps. Good luck a third time.

好的,希望帮助。祝你第三次好运。



UPDATE 3, AUG 2018

2018 年 8 月更新 3

TypeScript 3.0 introduced tuples in rest and spread positions, allowing us to easily deal with functions of an arbitrary number and type of arguments, without the above overloads and restrictions. Here's the new declaration of bindNew():

TypeScript 3.0在 rest 和 spread 位置引入了元组,使我们能够轻松处理任意数量和类型参数的函数,而没有上述重载和限制。这是 的新声明bindNew()

declare function bindNew<C extends { new(...args: A): T }, A extends any[], T>(
  Class: C & { new(...args: A): T }
): C & ((...args: A) => T);

回答by Paleo

The keyword newis required for ES6 classes:

newES6 类需要关键字:

However, you can only invoke a class via new, not via a function call (Sect. 9.2.2 in the spec) [source]

但是,您只能通过 new 调用类,而不能通过函数调用(规范中的第 9.2.2 节)[来源]

回答by rsp

Solution with instanceofand extendsworking

解决方案instanceofextends工作

The problem with most of the solution I've seen to use x = X()instead of x = new X()are:

与大多数我见过使用该解决方案的问题x = X(),而不是x = new X()有:

  1. x instanceof Xdoesn't work
  2. class Y extends X { }doesn't work
  3. console.log(x)prints some other type than X
  4. sometimes additionally x = X()works but x = new X()doesn't
  5. sometimes it doesn't work at all when targeting modern platforms (ES6)
  1. x instanceof X不起作用
  2. class Y extends X { }不起作用
  3. console.log(x)打印一些其他类型而不是 X
  4. 有时也x = X()有效但x = new X()无效
  5. 有时它在针对现代平台 (ES6) 时根本不起作用

My solutions

我的解决方案

TL;DR - Basic usage

TL;DR - 基本用法

Using the code below (also on GitHub - see: ts-no-new) you can write:

使用下面的代码(也在 GitHub 上 - 请参阅:ts-no-new)您可以编写:

interface A {
  x: number;
  a(): number;
}
const A = nn(
  class A implements A {
    x: number;
    constructor() {
      this.x = 0;
    }
    a() {
      return this.x += 1;
    }
  }
);

or:

或者:

class $A {
  x: number;
  constructor() {
    this.x = 10;
  }
  a() {
    return this.x += 1;
  }
}
type A = $A;
const A = nn($A);

instead of the usual:

而不是通常的:

class A {
  x: number;
  constructor() {
    this.x = 0;
  }
  a() {
    return this.x += 1;
  }
} 

to be able to use either a = new A()or a = A()with working instanceof, extends, proper inheritance and support for modern compilation targets (some solutions only work when transpiled to ES5 or older because they rely on classtranslated to functionwhich have different calling semantics).

能够为使用a = new A()a = A()与工作instanceofextends,适当的继承和支持现代编译目标(一些解决方案,只有工作的时候transpiled到ES5以上的老年人,因为他们依靠class翻译成function具有不同的调用语义)。

Full examples

完整示例

#1

#1

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

#2

#2

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

#3

#3

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

#2 simplified

#2 简化

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

class $B extends $A {
  a() {
    return this.x += 2;
  }
}
type B = $B;
const B: MC<B> = nn($B);

#3 simplified

#3 简化

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 = 10;
  }
  a() {
    return this.x += 1;
  }
}
type A = $A;
var A: MC<A> = nn($A);

class $B extends $A {
  a() {
    return this.x += 2;
  }
}
type B = $B;
var B: MC<B> = nn($B);

In #1and #2:

#1#2 中

  • instanceofworks
  • extendsworks
  • console.logprints correctly
  • constructorproperty of instances point to the real constructor
  • instanceof作品
  • extends作品
  • console.log打印正确
  • constructor实例的属性指向真正的构造函数

In #3:

#3 中

  • instanceofworks
  • extendsworks
  • console.logprints correctly
  • constructorproperty of instances point to the exposed wrapper (which may be an advantage or disadvantage depending on the circumstances)
  • instanceof作品
  • extends作品
  • console.log打印正确
  • constructor实例的属性指向暴露的包装器(根据情况,这可能是优点或缺点)

The simplifiedversions don't provide all meta-data for introspection if you don't need it.

如果您不需要,简化版本不会提供所有用于内省的元数据。

See also

也可以看看

回答by keos

My workaround with a type and a function:

我使用类型和函数的解决方法:

class _Point {
    public readonly x: number;
    public readonly y: number;

    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}
export type Point = _Point;
export function Point(x: number, y: number): Point {
    return new _Point(x, y);
}

or with an interface:

或使用接口:

export interface Point {
    readonly x: number;
    readonly y: number;
}

class _PointImpl implements Point {
    public readonly x: number;
    public readonly y: number;

    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

export function Point(x: number, y: number): Point {
    return new _PointImpl(x, y);
}

回答by N. Kudryavtsev

TL;DR

TL; 博士

If you are targeting ES6 and you really want to use classto store your data, not a function:

如果你的目标是 ES6 并且你真的想用它class来存储你的数据,而不是一个function

  • Create a functionthat simply invokes your class constructor with its arguments;
  • Set that function's prototypeto the prototypeof your class.
  • 创建一个function简单地用它的参数调用你的类构造函数;
  • 设置functionprototypeprototype你的类。

From now you are able to call that functioneither withor withoutnewkeyword to generate new class instances.

从现在开始,您function可以使用不使用new关键字调用它来生成新的类实例。

Typescript playground

打字稿游乐场



Typescript provides an ability to create such a function(let's call it a "callable constructor") in a strongly typed way. Well, anytype is necessary in intermediate type definitions (replacing it with unknowncauses errors), but this fact will notaffect your experience.

Typescript 提供了一种以function强类型方式创建这样一个(我们称之为“可调用构造函数”)的能力。好吧,any类型在中间类型定义中是必要的(用它代替会unknown导致错误),但这个事实不会影响您的体验。

First of all we need to define basic types to describe entities we are working with:

首先,我们需要定义基本类型来描述我们正在使用的实体:

// Let's assume "class X {}". X itself (it has type "typeof X") can be called with "new" keyword,
// thus "typeof X" extends this type
type Constructor = new(...args: Array<any>) => any;

// Extracts argument types from class constructor
type ConstructorArgs<TConstructor extends Constructor> =
    TConstructor extends new(...args: infer TArgs) => any ? TArgs : never;

// Extracts class instance type from class constructor
type ConstructorClass<TConstructor extends Constructor> =
    TConstructor extends new(...args: Array<any>) => infer TClass ? TClass : never;

// This is what we want: to be able to create new class instances
// either with or without "new" keyword
type CallableConstructor<TConstructor extends Constructor> =
  TConstructor & ((...args: ConstructorArgs<TConstructor>) => ConstructorClass<TConstructor>);

The next step is to write a function that accepts regular class constructors and creates corresponding "callable constructors".

下一步是编写一个函数,该函数接受常规类构造函数并创建相应的“可调用构造函数”。

function CreateCallableConstructor<TConstructor extends Constructor>(
    type: TConstructor
): CallableConstructor<TConstructor> {
    function createInstance(
        ...args: ConstructorArgs<TConstructor>
    ): ConstructorClass<TConstructor> {
        return new type(...args);
    }

    createInstance.prototype = type.prototype;
    return createInstance as CallableConstructor<TConstructor>;
}

Now all we have to do is to create our "callable constructor" and check it really works.

现在我们要做的就是创建我们的“可调用构造函数”并检查它是否真的有效。

class TestClass {
  constructor(readonly property: number) { }
}

const CallableTestConstructor = CreateCallableConstructor(TestClass);

const viaCall = CallableTestConstructor(56) // inferred type is TestClass
console.log(viaCall instanceof TestClass) // true
console.log(viaCall.property) // 56

const viaNew = new CallableTestConstructor(123) // inferred type is TestClass
console.log(viaNew instanceof TestClass) // true
console.log(viaNew.property) // 123

CallableTestConstructor('wrong_arg'); // error
new CallableTestConstructor('wrong_arg'); // error