Typescript: imansible accedere al valore del membro nel costruttore della class ereditata

Ho una class A , e una class B ereditato da essa.

 class A { constructor(){ this.init(); } init(){} } class B extends A { private myMember = {value:1}; constructor(){ super(); } init(){ console.log(this.myMember.value); } } const x = new B(); 

Quando eseguo questo codice, ottengo il seguente errore:

 Uncaught TypeError: Cannot read property 'value' of undefined 

Come posso evitare questo errore?

È chiaro per me che il codice JavaScript chiamerà il metodo init prima che crei myMember , ma ci dovrebbe essere qualche pratica / pattern per farlo funzionare.

Questo è il motivo per cui in alcuni linguaggi (cough C #) gli strumenti di analisi del codice segnalano l’utilizzo di membri virtuali all’interno dei costruttori.

Nel campo Typescript le inizializzazioni avvengono nel costruttore, dopo la chiamata al costruttore base. Il fatto che le inizializzazioni di campo siano scritte vicino al campo è solo zucchero sintattico. Se guardiamo il codice generato, il problema diventa chiaro:

 function B() { var _this = _super.call(this) || this; // base call here, field has not been set, init will be called _this.myMember = { value: 1 }; // field init here return _this; } 

Si dovrebbe prendere in considerazione una soluzione in cui init è chiamato dall’esterno dell’istanza e non nel costruttore:

 class A { constructor(){ } init(){} } class B extends A { private myMember = {value:1}; constructor(){ super(); } init(){ console.log(this.myMember.value); } } const x = new B(); x.init(); 

Oppure puoi avere un parametro extra per il tuo costruttore che specifica se chiamare init e non chiamarlo anche nella class derivata.

 class A { constructor() constructor(doInit: boolean) constructor(doInit?: boolean){ if(doInit || true)this.init(); } init(){} } class B extends A { private myMember = {value:1}; constructor() constructor(doInit: boolean) constructor(doInit?: boolean){ super(false); if(doInit || true)this.init(); } init(){ console.log(this.myMember.value); } } const x = new B(); 

O la soluzione molto molto sporca di setTimeout , che rinvierà l’inizializzazione fino al completamento del frame corrente. Ciò consentirà alla chiamata del costruttore padre di completare, ma ci sarà un interim tra la chiamata del costruttore e quando il timeout scade quando l’object non è stato init

 class A { constructor(){ setTimeout(()=> this.init(), 1); } init(){} } class B extends A { private myMember = {value:1}; constructor(){ super(); } init(){ console.log(this.myMember.value); } } const x = new B(); // x is not yet inited ! but will be soon 

Poiché la proprietà myMember è accessibile nel costruttore parent (si chiama init() durante la chiamata super() ), non esiste un modo in cui può essere definito nel costruttore figlio senza toccare una condizione di competizione.

Esistono diversi approcci alternativi.

hook di init

init è considerato un hook che non dovrebbe essere chiamato nel costruttore della class. Invece, è chiamato esplicitamente:

 new B(); B.init(); 

Oppure viene chiamato implicitamente dal framework, come parte del ciclo di vita dell’applicazione.

Proprietà statica

Se una proprietà dovrebbe essere una costante, può essere una proprietà statica.

Questo è il modo più efficiente perché questo è ciò che i membri statici sono per, ma la syntax potrebbe non essere attraente perché richiede di usare this.constructor anziché il nome della class se la proprietà statica dovrebbe essere correttamente riferita nelle classi this.constructor :

 class B extends A { static readonly myMember = { value: 1 }; init() { console.log((this.constructor as typeof B).myMember.value); } } 

Proprietà getter / setter

Il descrittore di proprietà può essere definito sul prototipo di class con la syntax get / set . Se una proprietà dovrebbe essere una costante primitiva, può essere solo un getter:

 class B extends A { get myMember() { return 1; } init() { console.log(this.myMember); } } 

Diventa più hacky se la proprietà non è costante o primitiva:

 class B extends A { private _myMember?: { value: number }; get myMember() { if (!('_myMember' in this)) { this._myMember = { value: 1 }; } return this._myMember!; } set myMember(v) { this._myMember = v; } init() { console.log(this.myMember.value); } } 

Inizializzazione sul posto

Una proprietà può essere inizializzata a cui si accede per prima. Se ciò accade nel metodo init cui è ansible accedervi prima del costruttore della class B , questo dovrebbe accadere lì:

 class B extends A { private myMember?: { value: number }; init() { this.myMember = { value: 1 }; console.log(this.myMember.value); } } 

Inizializzazione asincrona

init metodo init può diventare asincrono. Lo stato di inizializzazione dovrebbe essere tracciabile, quindi la class dovrebbe implementare alcune API per questo, ad esempio, in base alla promise:

 class A { initialization = Promise.resolve(); constructor(){ this.init(); } init(){} } class B extends A { private myMember = {value:1}; init(){ this.initialization = this.initialization.then(() => { console.log(this.myMember.value); }); } } const x = new B(); x.initialization.then(() => { // class is initialized }) 

Questo approccio può essere considerato antipattern in questo caso particolare perché la routine di inizializzazione è intrinsecamente sincrona, ma può essere adatta per le routine di inizializzazione asincrona.

Classe desugared

Poiché le classi ES6 hanno limitazioni sull’uso di this prima della super , la class figlio può essere sostituita da una funzione per eludere questa limitazione:

 interface B extends A {} interface BPrivate extends B { myMember: { value: number }; } interface BStatic extends A { new(): B; } const B = function B(this: BPrivate) { this.myMember = { value: 1 }; return A.call(this); } B.prototype.init = function () { console.log(this.myMember.value); } 

Questa è raramente una buona opzione, perché la class desugared dovrebbe essere digitata in TypScript. Anche questo non funzionerà con le classi genitore native (TypeScript es6 e esnext target).

Un approccio che è ansible adottare è utilizzare un getter / setter per myMember e gestire il valore predefinito nel getter. Ciò impedirebbe il problema non definito e ti permetterà di mantenere quasi esattamente la stessa struttura che hai. Come questo:

 class A { constructor(){ this.init(); } init(){} } class B extends A { private _myMember; constructor(){ super(); } init(){ console.log(this.myMember.value); } get myMember() { return this._myMember || { value: 1 }; } set myMember(val) { this._myMember = val; } } const x = new B(); 

Prova questo:

 class A { constructor() { this.init(); } init() { } } class B extends A { private myMember = { 'value': 1 }; constructor() { super(); } init() { this.myMember = { 'value': 1 }; console.log(this.myMember.value); } } const x = new B(); 

Super deve essere il primo comando. Ricorda che typescript è più “javascript con documentazione di tipi” piuttosto che linguaggio a sé stante.

Se si guarda il codice trans .iled è chiaramente visibile:

 class A { constructor() { this.init(); } init() { } } class B extends A { constructor() { super(); this.myMember = { value: 1 }; } init() { console.log(this.myMember.value); } } const x = new B(); 

Devi chiamare init in class A?

Funziona bene, ma non so se hai requisiti diversi:

 class A { constructor(){} init(){} } class B extends A { private myMember = {value:1}; constructor(){ super(); this.init(); } init(){ console.log(this.myMember.value); } } const x = new B(); 

Come questo :

  class A { myMember; constructor() { } show() { alert(this.myMember.value); } } class B extends A { public myMember = {value:1}; constructor() { super(); } } const test = new B; test.show();