Конструктор вызовов в классе 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?

Ответ 1

Как насчет этого? Опишите желаемую форму MyClass и его конструктор:

interface MyClass {
  val: number;
}

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

Обратите внимание, что MyClassConstructor определен как вызываемый как функция и как newable как конструктор. Затем осуществите это:

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, и компилятор не понимает, что возвращаемое значение MyClass соответствует вызываемой функции, а undefined значение соответствует конструктору newable... поэтому он жалуется. Следовательно, as MyClassConstructor в конце. Сморщьте два: this параметр в настоящее время не сужается с помощью анализа потока управления, поэтому мы должны утверждать, что this не является void при установке его свойства val, даже если в этот момент мы знаем, что он не может быть void. Поэтому мы должны использовать оператор ненулевого утверждения ! ,

В любом случае, вы можете убедиться, что они работают:

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

Надеюсь, это поможет; удачи!


ОБНОВИТЬ

Предостережение: как уже упоминалось в ответе @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

В этом случае вы переименовали MyClass таким образом, чтобы _MyClass, и определили MyClass как тип (такой же, как _MyClass) и значение (то же самое, что и конструктор _MyClass, но тип которого также может быть вызван как функция.) Это работает во время компиляции, как показано выше. Удовлетворяет ли ваше время выполнения это оговорками выше. Лично я бы придерживался стиля функции в своем исходном ответе, так как знаю, что они могут вызываться и обновляться в es2015 и более поздних версиях.

Еще раз удачи!


ОБНОВЛЕНИЕ 2

Если вы просто ищете способ объявления типа вашей функции 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 type is inferred as typeof _MyClass & ((a: number)=> _MyClass)

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

Но будьте осторожны, перегруженные объявления для bindNew() работают не во всех возможных случаях. В частности, это работает для конструкторов, которые принимают до трех обязательных параметров. Конструкторы с необязательными параметрами или множественными сигнатурами перегрузки, вероятно, не будут правильно выведены. Так что вам, возможно, придется подправить набор текста в зависимости от варианта использования.

Хорошо, надеюсь, это поможет. Удачи в третий раз.


ОБНОВЛЕНИЕ 3, АВГУСТ 2018

В TypeScript 3.0 введены кортежи в положениях покоя и разворота, что позволяет нам легко иметь дело с функциями с произвольным числом и типом аргументов, без вышеуказанных перегрузок и ограничений. Вот новая декларация bindNew():

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

Ответ 2

Ключевое слово new требуется для классов ES6:

Однако вы можете вызвать класс только через new, а не через вызов функции (раздел 9.2.2 в спецификации) [source]

Ответ 3

Решение с instanceof и extends работу

Проблема с большинством решений, которые я видел, чтобы использовать 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() с рабочим instanceof, extends, надлежащим наследованием и поддержкой современных целей компиляции (некоторые решения работают только при переносе в 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 свойство экземпляров указывают на экспонированных обертки (который может представлять собой преимущество или недостаток в зависимости от обстоятельств)

Упрощенные версии не предоставляют все метаданные для самоанализа, если вам это не нужно.

Смотрите также

Ответ 4

Мой обходной путь с типом и функцией:

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