Конструктор вызовов в классе 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()
:
-
x instanceof X
не работает -
class Y extends X { }
не работает -
console.log(x)
печатает другой тип, кроме X
- иногда дополнительно
x = X()
работает, но x = new X()
не работает - иногда это не работает вообще при таргетинге на современные платформы (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);
}