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?
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 this
n’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!
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!
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.
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);
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]
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);
}
instanceof
et extends
en marcheLe problème avec la plupart de la solution que j'ai vu d'utiliser Utiliser x = X()
au lieu de x = new X()
Est:
x instanceof X
ne fonctionne pasclass Y extends X { }
ne fonctionne pasconsole.log(x)
affiche un autre type que X
x = X()
fonctionne mais x = new X()
ne fonctionne pasEn 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).
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;
}
}
);
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' });
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' });
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);
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
œuvresextends
œuvresconsole.log
s'imprime correctementconstructor
des instances pointe vers le constructeur réelDans # 3 :
instanceof
œuvresextends
œuvresconsole.log
s'imprime correctementconstructor
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.