【问题标题】:Call constructor on TypeScript class without new在没有 new 的 TypeScript 类上调用构造函数
【发布时间】:2015-12-24 18:15:39
【问题描述】:

在 JavaScript 中,我可以定义一个构造函数,它可以在有或没有new 的情况下调用:

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

    this.val = val;
}

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

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

我尝试使用下面的 TypeScript 类来实现类似的结果:

class MyClass {
    val: number;

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

        this.val = val;
    }
}

但是调用 MyClass(5) 会给我错误 Value of type 'typeof MyClass' is not callable. Did you mean to include 'new'?

有什么方法可以让这种模式在 TypeScript 中工作?

【问题讨论】:

    标签: constructor typescript


    【解决方案1】:

    我喜欢@N。用于创建智能实例工厂的 Kudryavtsev 解决方案(使用 CreateCallableConstructor 包装的构造函数)。但是如果使用足够多的 any[] args,简单的 Reflect.construct(type, args) 就可以完美地工作。 以下是 mobx (v5) 的示例,它表明原型和装饰器没有问题:

    import { observable, reaction } from "mobx"; class TestClass { @observable stringProp: string; numProp: number; constructor(data: Partial) { if (data) { Object.assign(this, data); } } } var obj = Reflect.construct(TestClass, [{numProp: 123, stringProp: "foo"}]) as TestClass; // var obj = new TestClass({numProp: 123, stringProp: "foo"}); console.log(JSON.stringify(obj)); reaction(() => obj.stringProp, v => { console.log(v); } ); obj.stringProp = "bar";

    甚至这个简单的包装函数也可以工作:

    type Constructor = new (...args: any[]) => any; const createInstance = (c: Constructor, ...args) => new c(...args); var obj = createInstance(TestClass, {numProp: 123, stringProp: "foo"}); // or const createInstance1 = (c: Constructor) => (...args) => new c(...args); var obj1 = createInstance(TestClass)({numProp: 123, stringProp: "foo"}, 'bla');

    【讨论】:

      【解决方案2】:

      您可以使用const obj = Object.create(MyClass.prototype),然后使用Object.assign(obj, { foo: 'bar' }) 分配您想要的值

      这会在不使用new 关键字或构造函数的情况下创建一个类实例。

      【讨论】:

        【解决方案3】:

        这是我在jest 中解决此问题的方法,用于测试不可变模型组。 makeHash 函数没有做任何特别的事情,只是一个从 uuid() 创建简短随机字符串的实用程序。

        对我来说,“魔法”是将type 声明为new (...args: any[]) => any,允许它被“更新”为let model = new set.type(...Object.values(set.args));。所以,少说要绕过new,多了解以“新”形式工作。

        // models/oauth.ts
        export class OAuthEntity<T = string> {
          constructor(public readonly id: T) {}
          [key: string]: any;
        }
        
        export class OAuthClient extends OAuthEntity {
          /**
           * An OAuth Client
           * @param id A unique string identifying the client.
           * @param redirectUris Redirect URIs allowed for the client. Required for the authorization_code grant.
           * @param grants Grant types allowed for the client.
           * @param accessTokenLifetime Client-specific lifetime of generated access tokens in seconds.
           * @param refreshTokenLifetime Client-specific lifetime of generated refresh tokens in seconds
           * @param userId The user ID for client credential grants
           */
          constructor(
            public readonly id: string = '',
            public readonly redirectUris: string[] = [],
            public readonly grants: string[] = [],
            public readonly accessTokenLifetime: number = 0,
            public readonly refreshTokenLifetime: number = 0,
            public readonly userId?: string,
            public readonly privateKey?: string
          ) {
            super(id);
          }
        }
        
        // models/oauth.test.ts
        import { makeHash, makePin } from '@vespucci/utils';
        import { OAuthEntity, OAuthClient } from '@vespucci/admin/server/models/oauth';
        
        type ModelData = { type: new (...args: any[]) => any; args: { [key: string]: any }; defs?: { [key: string]: any } };
        
        describe('Model Tests', () => {
          const dataSet: ModelData[] = [
            { type: OAuthEntity, args: { id: makeHash() } },
            {
              type: OAuthClient,
              args: {
                id: makeHash(),
                redirectUris: [makeHash()],
                grants: [makeHash()],
                accessTokenLifetime: makePin(2),
                refreshTokenLifetime: makePin(2),
                userId: makeHash(),
                privateKey: makeHash(),
              },
            },
            {
              type: OAuthClient,
              args: {},
              defs: {
                id: '',
                redirectUris: [],
                grants: [],
                accessTokenLifetime: 0,
                refreshTokenLifetime: 0,
              },
            },
          ];
          dataSet.forEach((set) => {
            it(`Creates ${set.type.name} With ${Object.keys(set.args).length} Args As Expected`, () => {
              let model!: any;
              const checkKeys = Object.keys(set.args).concat(Object.keys(set.defs || {}).filter((k) => !(k in set.args)));
              const checkValues: any = checkKeys
                .map((key) => ({ [key]: set.args[key] || set.defs?.[key] }))
                .reduce((p, c) => ({ ...p, ...c }), {});
              expect(() => {
                model = new set.type(...Object.values(set.args));
              }).not.toThrow();
              expect(model).toBeDefined();
              checkKeys.forEach((key) => expect(model[key]).toEqual(checkValues[key]));
            });
          });
        });
        

        对我来说,最终结果是:

        【讨论】:

          【解决方案4】:

          TL;DR

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

          • 创建一个function,它只使用其参数调用您的类构造函数;
          • functionprototype 设置为您班级的prototype

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

          Typescript playground


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

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

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

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

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

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

          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
          

          【讨论】:

            【解决方案5】:

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

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

            或带有接口:

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

            【讨论】:

              【解决方案6】:

              instanceofextends 工作的解决方案

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

              1. x instanceof X 不起作用
              2. class Y extends X { } 不起作用
              3. console.log(x) 打印除X 之外的其他类型
              4. 有时x = X() 也有效,但x = new X() 无效
              5. 有时它在针对现代平台 (ES6) 时根本不起作用

              我的解决方案

              TL;DR - 基本用法

              使用下面的代码(也在 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;
                  }
                }
              );
              

              或:

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

              而不是通常的:

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

              能够使用a = new A()a = A() 使用instanceofextends、正确的继承和对现代编译目标的支持(某些解决方案仅在转译为 ES5 或更早版本时才有效,因为它们依赖于转换为具有不同调用语义的 classfunction)。

              完整示例

              #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

              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

              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 简化

              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 简化

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

              #1#2 中:

              • instanceof 工作
              • extends 工作
              • console.log 打印正确
              • 实例的constructor属性指向真正的构造函数

              #3中:

              • instanceof 工作
              • extends 工作
              • console.log 打印正确
              • 实例的constructor 属性指向暴露的包装器(根据具体情况,这可能是优点或缺点)

              如果您不需要,简化版不会提供所有元数据以供自省。

              另见

              【讨论】:

                【解决方案7】:

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

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

                请注意,MyClassConstructor 被定义为既可作为函数调用,又可作为构造函数来新建。然后实现它:

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

                上述工作,虽然有一些小皱纹。皱纹一:实现返回MyClass | undefined,编译器没有意识到MyClass返回值对应于可调用函数,undefined值对应于newable构造函数......所以它抱怨。因此最后是as MyClassConstructor。皱纹二:this 参数does not currently narrow via control flow analysis,因此我们必须在设置其val 属性时断言this 不是void,即使此时我们知道它不可能是void。所以我们必须使用non-null assertion operator !

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

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

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


                更新

                警告:如@Paleo 的answer 中所述,如果您的目标是 ES2015 或更高版本,则在源代码中使用 class 将在您编译的 JavaScript 中输出 class,而那些 require @ 987654344@ 根据规范。我见过像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
                

                在这种情况下,您已将MyClass 重命名为_MyClass,并将MyClass 定义为类型(与_MyClass 相同)和值(与@987654351 相同) @构造函数,但其​​类型被断言为也可以像函数一样调用。)这在编译时有效,如上所示。您的运行时是否对它满意取决于上述注意事项。就我个人而言,我会坚持原始答案中的函数样式,因为我知道它们在 es2015 及更高版本中都是可调用的和可更新的。

                再次祝你好运!


                更新 2

                如果您只是在寻找一种从 this answer 声明 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
                }
                

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

                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
                

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

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


                更新 3,2018 年 8 月

                TypeScript 3.0 引入了tuples in rest and spread positions,允许我们轻松处理任意数量和类型的参数的函数,而没有上述重载和限制。这是bindNew()的新声明:

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

                【讨论】:

                • 感谢您的回答!是否仍然可以使用class 语法和bindNew 函数或我在此处的回答中提到的classy-decorator 来协调这一点? stackoverflow.com/a/48326964/738768
                • 见上面的更新 2(第一次更新是我误读了你的问题;后一个更新解决了如何使用你的 bindNew() 函数进行协调)
                • 感谢您的详细说明。我给了你赏金。那么可能无法使用instanceof MyClass?另外,真的没有更好的方法来输入可变数量的参数吗?总的来说,这似乎比它的价值要麻烦得多:)
                • 我认为你可以让instanceof MyClass 工作,这取决于bindNew() 的实现,但我还没有尝试过。 ... 现在,从 v2.7 开始的 TypeScript 缺少一些类型运算符,您需要“正确”地执行可变数量的参数。您不能检查函数签名或 new() 签名来提取其参数类型(可能是某种元组)和返回类型。
                【解决方案8】:

                关键字 new 是 ES6 类所必需的:

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

                【讨论】:

                • 我遇到了同样的问题,但是声明描述了与上述类似的 ES5 模块。我的理解是,声明应该能够描述 ES5 行为,所以一定有办法做到这一点?
                • @dan 的前两个示例仍然是有效的 ES5 语法(带有和不带有 new 关键字),并且 Typescript 声明文件 (typescriptlang.org/docs/handbook/declaration-files/…) 应该能够描述用 ES5 编写的现有库,所以仍然必须有一些方法可以做到这一点......
                • @Andrew ES5 代码works with TS。但我不知道如何在定义文件中声明函数。您应该创建一个新问题。然后,在这里给出链接。 ;)
                猜你喜欢
                • 2018-10-20
                • 2017-09-29
                • 2018-09-19
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2018-11-12
                • 2019-04-21
                相关资源
                最近更新 更多