TypeScript sınıfını new kullanmadan çağırmak


JavaScript'te, new ile veya new olmadan çağrılabilecek bir yapıcı (constructor) fonksiyon tanımlayabilirim:

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

    this.val = val;
}

Daha sonra MyClass nesnelerini aşağıdaki ifadelerden herhangi birini kullanarak oluşturabilirim:

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

Aşağıdaki TypeScript sınıfını kullanarak benzer bir sonucu elde etmeye çalıştım:

class MyClass {
    val: number;

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

        this.val = val;
    }
}

Ama MyClass(5) çağırmak bana Value of type 'typeof MyClass' is not callable. Did you mean to include 'new'? hatasını veriyor

Bu deseni TypeScript'te çalıştırmanın bir yolu var mı?

Cevap #1

Buna ne dersiniz? MyClass ve yapıcısının istenen şeklini tanımlayın:

interface MyClass {
  val: number;
}

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

Dikkat edin ki MyClassConstructor, hem bir fonksiyon olarak çağrılabilir hem de bir yapıcı (constructor) olarak kullanılabilir şekilde tanımlanmıştır. Sonra bunu uygulayın:

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

Yukarıdaki yöntem çalışıyor, ancak birkaç küçük pürüz var. Pürüz bir: uygulama MyClass | undefined döndürüyor ve derleyici MyClass dönüş değerinin çağrılabilir fonksiyona ve undefined değerinin newlenebilir yapıcıya karşılık geldiğini fark etmiyor... bu yüzden hata veriyor. Bu nedenle sonunda as MyClassConstructor kullanılıyor. Pürüz iki: this parametresi şu anda kontrol akışı analizine göre daralmıyor, bu yüzden val özelliğini ayarlarken this'in void olmadığını belirtmemiz gerekiyor, o noktada onun void olamayacağını bilsek bile. Bu yüzden null olmayan atama operatörü !'u kullanmamız gerekiyor.

Her neyse, bunların çalıştığını doğrulayabilirsiniz:

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

Umarım yardımcı olur; iyi şanslar!

GÜNCELLE

Uyarı: @Paleo'nun cevabında belirtildiği gibi, eğer hedefiniz ES2015 veya daha sonraki bir sürümse, kaynağınızda class kullanmak, derlenmiş JavaScript'te class olarak çıkacaktır ve bunlar spesifikasyona göre new() gerektirir. TypeError: Class constructors cannot be invoked without 'new' gibi hatalar gördüm. Bazı JavaScript motorlarının spesifikasyonu görmezden gelmesi ve fonksiyon tarzı çağrıları da memnuniyetle kabul etmesi oldukça mümkündür. Bu uyarılara aldırmıyorsanız (örneğin hedefiniz açıkça ES5 ise veya spesifikasyona uymayan ortamlardan birinde çalıştıracağınızı biliyorsanız), o zaman TypeScript'i kesinlikle buna uygun hale getirebilirsiniz:

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

Bu durumda MyClass'i _MyClass olarak yeniden adlandırdınız ve MyClass'i hem bir tip (tıpkı _MyClass gibi) hem de bir değer (tıpkı _MyClass yapıcı fonksiyonu gibi, ancak tipi aynı zamanda bir fonksiyon gibi çağrılabilir olarak onaylanmış) olarak tanımladınız. Bu, yukarıda görüldüğü gibi derleme zamanında çalışır. Çalışma zamanında bunun sizin ortamınız tarafından kabul edilip edilmeyeceği yukarıdaki uyarılara bağlıdır. Kişisel olarak, orijinal cevabımda belirttiğim fonksiyon tarzına bağlı kalırdım çünkü bunların es2015 ve sonrası sürümlerde hem çağrılabilir hem de new ile oluşturulabilir olduğunu biliyorum.

Tekrar iyi şanslar!

GÜNCELLEME 2

Eğer sadece bu cevaptaki bindNew() fonksiyonunuzun tipini belirtmenin bir yolunu arıyorsanız, bu fonksiyon spec'e uygun bir class alır ve hem newlenebilir hem de bir fonksiyon gibi çağrılabilir bir şey üretir, şu şekilde yapabilirsiniz:

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
}

Bu, bunu doğru şekilde yazmanın etkisine sahiptir:

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

Ama dikkat edin, bindNew() için aşırı yüklenmiş bildirimler her durum için çalışmaz. Özellikle, en fazla üç zorunlu parametre alan yapıcılar için çalışır. Opsiyonel parametreleri olan ya da birden fazla aşırı yük imzasına sahip yapıcılar muhtemelen doğru şekilde çıkarılamaz. Bu nedenle, kullanım durumuna bağlı olarak tip tanımlarını ayarlamanız gerekebilir.

Tamam, umarım bu yardımcı olur. Üçüncü kez iyi şanslar.

GÜNCELLEME 3, AĞU 2018

TypeScript 3.0, rest ve spread konumlarında tuple'ları tanıttı ve bu sayede yukarıdaki aşırı yüklemeler ve kısıtlamalar olmadan keyfi sayıda ve türde argüman alan fonksiyonlarla kolayca başa çıkmamıza olanak sağladı. İşte bindNew() fonksiyonunun yeni bildirimi:

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

Cevap #2

instanceof ve extends ile çalışan çözüm

Gördüğüm çözümlerin çoğunun x=new X() yerine x=X() kullanmasıyla ilgili sorunları şunlardır:

  1. x instanceof X çalışmıyor
  2. class Y extends X { } çalışmıyor
  3. console.log(x), X'ten farklı bir tür yazdırır
  4. bazen ek olarak x=X() çalışır ama x=new X() çalışmaz
  5. bazen modern platformları (ES6) hedeflerken hiç çalışmaz

Benim çözümlerim

Özet - Temel kullanım

Aşağıdaki kodu kullanarak (GitHub’da da mevcut - bakınız: ts-no-new) şunu yazabilirsiniz:

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

veya :

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

alışılmışın yerine:

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

hem a=new A() hem de a=A() kullanabilmek için, çalışan instanceof, extends, doğru miras yapısı ve modern derleme hedefleri desteği ile (bazı çözümler yalnızca class'ın function'a çevrildiği ve farklı çağrı semantiklerine sahip olduğu durumlarda ES5 veya daha eski sürümlere dönüştürüldüğünde çalışır).

Tam örnekler

#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 basitleştirilmiş

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 basitleştirilmiş

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 ve #2 içinde:

  • instanceof çalışır
  • extends çalışıyor
  • console.log doğru şekilde yazdırır
  • Örneklerin constructor özelliği gerçek yapıcıyı gösterir

#3 içinde:

  • instanceof çalışır
  • extends çalışıyor
  • console.log doğru şekilde yazdırır
  • Örneklerin constructor özelliği, duruma bağlı olarak bir avantaj veya dezavantaj olabilecek ortaya çıkarılmış sarmalayıcıya işaret eder

Basitleştirilmiş sürümler, bunu ihtiyaç duymuyorsanız introspeksiyon için tüm meta verileri sağlamaz.

Ayrıca bakınız

  • Benim cevabım: TypeScript'te, bir sınıf "new" anahtar kelimesi olmadan kullanılabilir mi?
  • Daha fazla örnek içeren GitHub depo'm: https://github.com/rsp/ts-no-new

İlgili makaleler