TypeScript: clase de error extensible

81

Estoy tratando de lanzar un error personalizado con mi nombre de clase "CustomError" impreso en la consola en lugar de "Error", sin éxito:

class CustomError extends Error { 
    constructor(message: string) {
      super(`Lorem "${message}" ipsum dolor.`);
      this.name = 'CustomError';
    }
}
throw new CustomError('foo'); 

La salida es Uncaught Error: Lorem "foo" ipsum dolor.

Lo que espero: Uncaught CustomError: Lorem "foo" ipsum dolor.

Me pregunto si eso se puede hacer usando solo TS (sin jugar con los prototipos JS).

canto alma oscura
fuente

Respuestas:

78

¿Está utilizando mecanografiado la versión 2.1 y transpilando a ES5? Consulte esta sección de la página de cambios importantes para ver posibles problemas y soluciones: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array- y el mapa puede que ya no funcione

El bit relevante:

Como recomendación, puede ajustar manualmente el prototipo inmediatamente después de cualquier llamada de super (...).

class FooError extends Error {
    constructor(m: string) {
        super(m);

        // Set the prototype explicitly.
        Object.setPrototypeOf(this, FooError.prototype);
    }

    sayHello() {
        return "hello " + this.message;
    }
}

Sin embargo, cualquier subclase de FooError también tendrá que configurar manualmente el prototipo. Para los tiempos de ejecución que no son compatibles con Object.setPrototypeOf, es posible que pueda usar __proto__.

Desafortunadamente, estas soluciones no funcionarán en Internet Explorer 10 y versiones anteriores. Uno puede copiar manualmente métodos del prototipo en la instancia en sí (es decir, FooError.prototype en esta), pero la cadena del prototipo en sí no se puede arreglar.

Vidar
fuente
2
Sí, me estoy trasladando a ES5. Traté de configurar el prototipo como sugirió, pero desafortunadamente, no funcionó.
darksoulsong
3
Vale la pena señalar que, lamentablemente, Object.setPrototypeOf(this, this.constructor.prototype)será no trabajar aquí, la clase tiene que hacer referencia de forma explícita.
John Weisz
1
@JohnWeisz si la solución que está mencionando funcionó, entonces no habrá nada que arreglar en primer lugar, si pudiera leer el prototipo this.constructoren este punto, entonces la cadena de prototipos ya estaría intacta.
thetrompf
3
A partir de TypeScript 2.2, parece haber una manera de hacerlo sin codificar el tipo exacto en el constructor a través de: Object.setPrototypeOf(this, new.target.prototype); typescriptlang.org/docs/handbook/release-notes/…
Grabofus
65

El problema es que la clase incorporada de Javascript Errorrompe la cadena de prototipos al cambiar el objeto que se va a construir (es decir this) a un objeto nuevo y diferente, cuando llama supery ese nuevo objeto no tiene la cadena de prototipos esperada, es decir, es una instancia de Errorno de CustomError.

Este problema se puede resolver elegantemente usando 'new.target', que es compatible desde Typecript 2.2, consulte aquí: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html

class CustomError extends Error {
  constructor(message?: string) {
    // 'Error' breaks prototype chain here
    super(message); 

    // restore prototype chain   
    const actualProto = new.target.prototype;

    if (Object.setPrototypeOf) { Object.setPrototypeOf(this, actualProto); } 
    else { this.__proto__ = actualProto; } 
  }
}

El uso new.targettiene la ventaja de que no tiene que codificar el prototipo, como algunas otras respuestas propuestas aquí. De nuevo, eso tiene la ventaja de que las clases heredadas CustomErrortambién obtendrán automáticamente la cadena de prototipos correcta.

Si tuviera que codificar el prototipo (p Object.setPrototype(this, CustomError.prototype). Ej. ), El CustomErrormismo tendría una cadena de prototipos en funcionamiento, pero cualquier clase heredada CustomErrorse rompería, p. Ej., Las instancias de a class VeryCustomError < CustomErrorno serían las instanceof VeryCustomErroresperadas, sino únicamente instanceof CustomError.

Véase también: https://github.com/Microsoft/TypeScript/issues/13965#issuecomment-278570200

Kristian Hanekamp
fuente
¿Debe this.__proto__ser público o privado?
lonix
Puede hacerlo privado, creo, ya que no intenta acceder a él fuera del constructor. Por lo tanto, siempre que solo lo use en TypeScript que se compila en ES5, no debería ser un problema, ya que esta función de TypeScript es solo una función de tiempo de compilación. El campo siempre será de acceso público en su código compilado. Actualmente, la propuesta actual de "campos de clases privados" se encuentra solo en etapa experimental. Y la __proto__propiedad también está obsoleta y solo un poco estandarizada para ser compatible con versiones anteriores. Consulte MDN para obtener más información sobre ambos.
Trve
1
O simplemente puede hacer algo como (this as any).__proto__ = actualProto;. Esto es lo que hice. Hacky, pero no necesito cambiar ninguna declaración de clase.
Trve
18

Funciona correctamente en ES2015 ( https://jsfiddle.net/x40n2gyr/ ). Lo más probable es que el problema sea que el compilador de TypeScript se está transfiriendo a ES5 y Errorno se puede subclasificar correctamente utilizando solo las funciones de ES5; solo se puede subclasificar correctamente usando ES2015 y características superiores ( classo, de manera más oscura, Reflect.construct). Esto se debe a que cuando llama Errorcomo una función (en lugar de a través de newo, en ES2015, supero Reflect.construct), ignora thisy crea un nuevo Error .

Probablemente tendrá que vivir con la salida imperfecta hasta que pueda apuntar a ES2015 o superior ...

TJ Crowder
fuente
2
Esta. Esta. Esto y nuevamente esto.
Luca T
Como se mencionó en otras respuestas aquí, ya existe una solución alternativa, por lo que es mejor aceptar la complejidad adicional e implementar la actualización de la cadena de prototipos.
JoshuaTree
10

A partir de TypeScript 2.2 se puede hacer a través de new.target.prototype. https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#example

class CustomError extends Error {
    constructor(message?: string) {
        super(message); // 'Error' breaks prototype chain here
        this.name = 'CustomError';
        Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
    }
}
Grabofus
fuente
1
Esto no establece la propiedad .name
retorquere
6

Me encontré con el mismo problema en mi proyecto de mecanografiado hace unos días. Para que funcione, utilizo la implementación de MDN usando solo vanilla js. Entonces, su error se vería como lo siguiente:

function CustomError(message) {
  this.name = 'CustomError';
  this.message = message || 'Default Message';
  this.stack = (new Error()).stack;
}
CustomError.prototype = Object.create(Error.prototype);
CustomError.prototype.constructor = CustomError;

throw new CustomError('foo');

No parece funcionar en el fragmento de código SO, pero sí en la consola de Chrome y en mi proyecto de mecanografiado:

ingrese la descripción de la imagen aquí

Mμ.
fuente
1
Me gustó la idea, no la implementé en el texto mecanografiado
Julius Š
De Verdad? Funcionó en mi proyecto nativo de reacción mecanografiado.
Mμ.
Bueno, también quería abstraer la parte repetitiva y probablemente no lo logré, lo intentaré de nuevo sin ningún otro shinanigens.
Julius Š.
Buena idea, pero falla con este mensaje cuando está strict: trueen tsconfig: stackoverflow.com/questions/43623461/…
André
Funciona, pero rompe el formato de error en la consola del navegador. En la consola de Chrome, sus instancias de CustomError no se muestran con el formato habitual más agradable que los errores "verdaderos", sino en un formato de "objeto genérico", que es más difícil de leer.
Kristian Hanekamp
2

Literalmente nunca publico en SO, pero mi equipo está trabajando en un proyecto de TypeScript, y necesitábamos crear muchas clases de error personalizadas, mientras también apuntábamos a es5. Habría sido increíblemente tedioso hacer la solución sugerida en cada clase de error. Pero descubrimos que podíamos tener un efecto descendente en todas las clases de error posteriores al crear una clase de error personalizada principal y tener el resto de nuestros errores en extendesa clase. Dentro de esa clase de error principal, hicimos lo siguiente para tener ese efecto descendente de actualizar el prototipo:

class MainErrorClass extends Error {
  constructor() {
    super()
    Object.setPrototypeOf(this, new.target.prototype)
  }
}

class SomeNewError extends MainErrorClass {} 

...

El uso new.target.prototypefue la clave para actualizar todas las clases de error heredadas sin necesidad de actualizar el constructor de cada una.

¡Solo espero que esto le ahorre a otra persona un dolor de cabeza en el futuro!

EmiPixi
fuente
1

Prueba esto...

class CustomError extends Error { 

  constructor(message: string) {
    super(`Lorem "${message}" ipsum dolor.`)
  }

  get name() { return this.constructor.name }

}

throw new CustomError('foo')
flcoder
fuente
Pero esto no parece utilizar la funcionalidad ya existente y nos obliga a volver a implementar la rueda, por así decirlo. ¿O me equivoco?
JoshuaTree
@JoshuaTree ¿Qué funcionalidad ya existente no utiliza la solución? ¿Qué rueda se está volviendo a implementar? Puede que no se equivoque, pero sus argumentos podrían ser un poco más específicos.
flcoder
Perdón mi error. Revisé el src mecanografiado y veo que es una interfaz, no un objeto real con getters y setters. Su implementación no anula ninguna funcionalidad existente.
JoshuaTree