Typescript: no se puede acceder al valor de miembro en el constructor de clases heredado

Tengo una clase A y una clase B heredada de ella.

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

Cuando ejecuto este código, aparece el siguiente error:

 Uncaught TypeError: Cannot read property 'value' of undefined 

¿Cómo puedo evitar este error?

Para mí está claro que el código JavaScript llamará al método init antes de que cree el myMember , pero debería haber algo de práctica / patrón para hacerlo funcionar.

Esta es la razón por la cual en algunos idiomas (tos C #) las herramientas de análisis de código marcan el uso de miembros virtuales dentro de los constructores.

En las inicializaciones de campo de Typescript ocurren en el constructor, después de la llamada al constructor base. El hecho de que las inicializaciones de campo se escriben cerca del campo es solo azúcar sintáctico. Si miramos el código generado, el problema se vuelve claro:

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

Debería considerar una solución donde init se invoca desde fuera de la instancia y no desde el constructor:

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

O puede tener un parámetro adicional para su constructor que especifique si llamar a init y no llamarlo también en la clase derivada.

 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 solución muy muy sucia de setTimeout , que retrasará la inicialización hasta que se complete el marco actual. Esto permitirá que se complete la llamada del constructor padre, pero habrá un intervalo entre la llamada del constructor y cuando expire el tiempo de espera cuando el objeto no se haya 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 

Debido a que se accede a la propiedad myMember en el constructor padre (se llama a init() durante la llamada a super() ), no hay forma de cómo se puede definir en el constructor hijo sin alcanzar una condición de carrera.

Hay varios enfoques alternativos.

gancho de init

init se considera un enlace que no se debe llamar en el constructor de la clase. En cambio, se llama explícitamente:

 new B(); B.init(); 

O se llama implícitamente por el marco, como parte del ciclo de vida de la aplicación.

Propiedad estática

Si se supone que una propiedad es una constante, puede ser una propiedad estática.

Esta es la forma más eficiente porque para esto están los miembros estáticos, pero la syntax puede no ser tan atractiva porque requiere usar this.constructor lugar del nombre de clase si la propiedad estática se debe referir adecuadamente en las clases secundarias:

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

Property getter / setter

El descriptor de propiedad se puede definir en prototipo de clase con syntax get / set . Si se supone que una propiedad es primitiva, puede ser solo un getter:

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

Se vuelve más hacky si la propiedad no es constante 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); } } 

Inicialización en el lugar

Una propiedad puede inicializarse donde se acceda primero. Si esto sucede en el método init donde se puede acceder a esto antes del constructor de la clase B , esto debería ocurrir allí:

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

Inicialización asincrónica

init método init puede volverse asincrónico. El estado de inicialización debe ser rastreable, por lo que la clase debe implementar alguna API para eso, por ejemplo, basado en promesas:

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

Este enfoque puede considerarse antipatrón para este caso particular porque la rutina de inicialización es intrínsecamente sincrónica, pero puede ser adecuada para rutinas de inicialización asincrónicas.

Clase Desugared

Dado que las clases de ES6 tienen limitaciones en el uso de this antes de super , la clase de niño se puede desagrupar a una función para evadir esta limitación:

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

Esta rara vez es una buena opción, ya que la clase desugared también debe escribirse en TypeScript. Esto tampoco funcionará con las clases padre nativas (TypeScript es6 y esnext target).

Un enfoque que podría tomar es usar un getter / setter para myMember y administrar el valor predeterminado en el getter. Esto evitaría el problema indefinido y le permitirá mantener casi exactamente la misma estructura que tiene. Me gusta esto:

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

Prueba esto:

 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 tiene que ser el primer comando. Recuerde que el texto typescript es más “javascript con documentación de tipos” que lenguaje por sí mismo.

Si miras el código transstackdo .js, es claramente visible:

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

¿Tienes que llamar a init en la clase A?

Eso funciona bien, pero no sé si tiene diferentes requisitos:

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

Me gusta esto :

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