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

时间:2015-09-27 10:55:03

标签: constructor typescript

在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中运行吗?

6 个答案:

答案 0 :(得分:15)

这个怎么样?描述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;

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

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

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

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

更新

警告:正如@ Paleo answer中所述,如果您的目标是ES2015或更高版本,则在源代码中使用class将在已编译的JavaScript中输出class,并且那些需要 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

在这种情况下,您已将MyClass重命名为_MyClass,并将MyClass定义为两种类型(与_MyClass相同)和一个值(与_MyClass构造函数相同,但其类型被声明为可以像函数一样调用。)这在编译时工作,如上所示。您的运行时是否满意它受上述警告的影响。我个人在我的原始答案中坚持使用函数风格,因为我知道这些函数在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()的重载声明不适用于每种可能的情况。具体来说,它适用于最多需要三个参数的构造函数。具有可选参数或多个过载签名的构造函数可能无法正确推断。因此,您可能需要根据用例调整输入。

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

2018年8月3日更新

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

答案 1 :(得分:5)

ES6类需要关键字new

  

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

答案 2 :(得分:0)

使用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或更早版本时才有效,因为它们依赖于class转换为function具有不同的调用语义)。

完整示例

#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实例的属性指向暴露的包装器(根据情况可能是优缺点)

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

另请参见

答案 3 :(得分:0)

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

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

答案 4 :(得分:0)

TL; DR

如果您以ES6为目标,并且确实要使用class来存储数据,而不是function

  • 创建一个function,它只需使用其参数调用您的类构造函数即可;
  • function的{​​{1}}设置为班级的prototype

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

Typescript playground


Typescript提供了一种以强类型创建此类new(称为“可调用构造函数”)的功能。好吧,在中间类型定义中,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>;
}

答案 5 :(得分:0)

这是我在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]));
    });
  });
});

对我来说,最终结果是:

enter image description here