web-dev-qa-db-fra.com

Appeler le constructeur sur la classe TypeScript sans nouveau

En JavaScript, je peux définir une fonction constructeur pouvant être appelée avec ou sans new:

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

    this.val = val;
}

Je peux ensuite construire des objets MyClass en utilisant l'une des déclarations suivantes:

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

J'ai essayé d'obtenir un résultat similaire en utilisant la classe TypeScript ci-dessous:

class MyClass {
    val: number;

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

        this.val = val;
    }
}

Mais appeler MyClass(5) me donne l'erreur Value of type 'typeof MyClass' is not callable. Did you mean to include 'new'?

Est-il possible de faire fonctionner ce modèle dans TypeScript?

12
dan

Et ça? Décrivez la forme désirée de MyClass et son constructeur:

interface MyClass {
  val: number;
}

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

Notez que MyClassConstructor est défini à la fois comme appelable en tant que fonction et newable en tant que constructeur. Puis implémentez-le:

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

Ce qui précède fonctionne bien qu'il y ait quelques petites rides. Wrinkle one: l'implémentation retourne MyClass | undefined et le compilateur ne se rend pas compte que la valeur retournée MyClass correspond à la fonction callable et que la valeur undefined correspond au constructeur newable ... donc, elle se plaint. D'où le as MyClassConstructor à la fin. Corrigez deux: le paramètre thisn’est actuellement pas étroit via l’analyse de flux de contrôle , nous devons donc affirmer que this n’est pas void lorsqu’il définit sa propriété val, même si nous savons qu’elle ne peut pas être void. Nous devons donc utiliser l'opérateur d'assertion non-null !

Quoi qu'il en soit, vous pouvez vérifier que ceux-ci fonctionnent:

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

J'espère que cela pourra aider; bonne chance!


METTRE À JOUR

Mise en garde: comme mentionné dans la answer de @ Paleo, si votre cible est ES2015 ou ultérieure, utiliser class dans votre source affichera class dans votre code JavaScript compilé, et ceux require new() selon les spécifications. J'ai vu des erreurs comme TypeError: Class constructors cannot be invoked without 'new'. Il est tout à fait possible que certains moteurs JavaScript ignorent la spécification et acceptent volontiers les appels de style fonction également. Si vous ne vous souciez pas de ces mises en garde (par exemple, votre cible est explicitement ES5 ou si vous savez que vous allez vous lancer dans l'un de ces environnements non conformes aux spécifications), vous pouvez forcer TypeScript à accepter:

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

Dans ce cas, vous avez renommé MyClass en _MyClass et défini MyClass comme étant à la fois un type (identique à _MyClass) et une valeur (identique au constructeur _MyClass, mais dont le type est également appelé comme suit: une fonction.) Cela fonctionne au moment de la compilation, comme on le voit ci-dessus. Que votre exécution soit satisfaite ou non dépend des avertissements ci-dessus. Personnellement, je me contenterais du style de la fonction dans ma réponse initiale, car je sais que ces fonctions sont appelables et novatrices dans es2015 et les versions ultérieures. 

Bonne chance encore!


MISE À JOUR 2

Si vous cherchez simplement un moyen de déclarer le type de votre fonction bindNew() à partir de cette réponse , qui prend une class conforme aux spécifications et produit quelque chose qui est à la fois nouvelle et appelable comme une fonction, vous pouvez faire quelque chose comme: ce:

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
}

Cela a pour effet de taper correctement ceci:

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

Mais attention, les déclarations surchargées de bindNew() ne fonctionnent pas dans tous les cas. Plus précisément, cela fonctionne pour les constructeurs qui prennent jusqu'à trois paramètres requis. Les constructeurs avec des paramètres facultatifs ou plusieurs signatures de surcharge ne seront probablement pas correctement déduits. Vous devrez donc peut-être modifier les typages en fonction du cas d'utilisation.

Ok, espérons que aide. Bonne chance une troisième fois.


MISE À JOUR 3, août 2018

TypeScript 3.0 a introduit les tuples dans des positions de repos et étendues , nous permettant de traiter facilement les fonctions d’un nombre arbitraire et du type d’arguments, sans les surcharges et les restrictions susmentionnées. Voici la nouvelle déclaration de bindNew():

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

Le mot clé new est requis pour les classes ES6:

Cependant, vous ne pouvez appeler une classe que via new et non via un appel de fonction (Sect. 9.2.2 dans la spécification) [source]

5
Paleo

Ma solution de contournement avec un type et une fonction:

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

Solution avec instanceof et extends en marche

Le problème avec la plupart de la solution que j'ai vu d'utiliser Utiliser x = X() au lieu de x = new X() Est:

  1. x instanceof X ne fonctionne pas
  2. class Y extends X { } ne fonctionne pas
  3. console.log(x) affiche un autre type que X
  4. parfois, en plus x = X() fonctionne mais x = new X() ne fonctionne pas
  5. parfois, cela ne fonctionne pas du tout lorsque vous ciblez des plates-formes modernes (ES6)

Mes solutions

TL; DR - Utilisation de base

En utilisant le code ci-dessous (également sur GitHub - voir: ts-no-new ), vous pouvez écrire:

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

ou:

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

au lieu de l'habituel:

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

pour pouvoir utiliser a = new A() ou a = A() avec instanceof, extends de travail, un héritage approprié et la prise en charge des cibles de compilation modernes (certaines solutions ne fonctionnent que si transpilé en ES5 ou version antérieure, car elles reposent sur class traduit en function qui sémantique d’appel).

Exemples complets

#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 simplifié

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 simplifié

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

Dans # 1 et # 2 :

  • instanceof œuvres
  • extends œuvres
  • console.log s'imprime correctement
  • La propriété constructor des instances pointe vers le constructeur réel

Dans # 3 :

  • instanceof œuvres
  • extends œuvres
  • console.log s'imprime correctement
  • La propriété constructor des instances pointe vers le wrapper exposé (ce qui peut constituer un avantage ou un inconvénient selon les circonstances)

Les versions simplifiées ne fournissent pas toutes les métadonnées pour l'introspection si vous n'en avez pas besoin.

Voir également

0
rsp