Typescript: Sugerencia: cláusula `throws` y cláusula catch escrita

Creado en 29 dic. 2016  ·  135Comentarios  ·  Fuente: microsoft/TypeScript

El sistema de tipos mecanografiados es útil en la mayoría de los casos, pero no se puede utilizar cuando se manejan excepciones.
Por ejemplo:

function fn(num: number): void {
    if (num === 0) {
        throw "error: can't deal with 0";
    }
}

El problema aquí es doble (sin mirar el código):

  1. Al usar esta función, no hay forma de saber que podría arrojar un error
  2. No está claro cuál será el tipo de error

En muchos escenarios, esto no es realmente un problema, pero saber si una función/método puede generar una excepción puede ser muy útil en diferentes escenarios, especialmente cuando se usan diferentes bibliotecas.

Al introducir la excepción marcada (opcional), el sistema de tipos se puede utilizar para el manejo de excepciones.
Sé que las excepciones verificadas no están acordadas (por ejemplo, Anders Hejlsberg ), pero al hacerlo opcional (¿y tal vez inferir? más adelante), solo agrega la oportunidad de agregar más información sobre el código que puede ayudar a los desarrolladores, herramientas y documentación.
También permitirá un mejor uso de errores personalizados significativos para grandes proyectos grandes.

Como todos los errores de tiempo de ejecución de javascript son de tipo Error (o tipos de extensión como TypeError ), el tipo real de una función siempre será type | Error .

La gramática es sencilla, una definición de función puede terminar con una cláusula throws seguida de un tipo:

function fn() throws string { ... }
function fn(...) throws string | number { ... }

class MyError extends Error { ... }
function fn(...): Promise<string> throws MyError { ... }

Al capturar las excepciones, la sintaxis es la misma con la capacidad de declarar los tipos de error:
catch(e: string | Error) { ... }

Ejemplos:

function fn(num: number): void throws string {
    if (num === 0) {
        throw "error: can't deal with 0";
    }
}

Aquí está claro que la función puede arrojar un error y que el error será una cadena, por lo que al llamar a este método, el desarrollador (y el compilador/IDE) lo conocen y pueden manejarlo mejor.
Entonces:

fn(0);

// or
try {
    fn(0); 
} catch (e: string) { ... }

Compila sin errores, pero:

try {
    fn(0); 
} catch (e: number) { ... }

No se puede compilar porque number no es string .

Inferencia de tipo de error y flujo de control

try {
    fn(0);
} catch(e) {
    if (typeof e === "string") {
        console.log(e.length);
    } else if (e instanceof Error) {
        console.log(e.message);
    } else if (typeof e === "string") {
        console.log(e * 3); // error: Unreachable code detected
    }

    console.log(e * 3); // error: The left-hand side of an arithmetic operation must be of type 'any', 'number' or an enum type
}
function fn(num: number): void {
    if (num === 0) {
        throw "error: can't deal with 0";
    }
}

Lanza string .

function fn2(num: number) {
    if (num < 0) {
        throw new MyError("can only deal with positives");
    }

    fn(num);
}

Tira MyError | string .
Sin embargo:

function fn2(num: number) {
    if (num < 0) {
        throw new MyError("can only deal with positives");
    }

    try {
        fn(num);
    } catch(e) {
        if (typeof e === "string") {
           throw new MyError(e);
       } 
    }
}

Tira solo MyError .

Awaiting More Feedback Suggestion

Comentario más útil

@aleksey-bykov

Estás sugiriendo no usar throw en absoluto en mi código y en su lugar envolver los resultados (en funciones que pueden generar errores).
Este enfoque tiene algunos inconvenientes:

  • Este envoltorio crea más código.
  • Requiere que toda la cadena de función invocada devuelva este valor envuelto (o error) o, alternativamente, la función que obtiene Tried<> no puede elegir ignorar el error.
  • No es un estándar, las bibliotecas de terceros y los js nativos arrojan errores

Agregar throws permitirá a los desarrolladores que elijan manejar errores de su código, bibliotecas 3rd y js nativo.
Como la sugerencia también solicita la inferencia de errores, todos los archivos de definición generados pueden incluir la cláusula throws .
Será muy conveniente saber qué errores puede arrojar una función directamente desde el archivo de definición en lugar del estado actual donde debe ir a los documentos, por ejemplo, para saber qué error JSON.parse podría arrojar. a la página de MDN y lea eso:

Lanza una excepción SyntaxError si la cadena a analizar no es JSON válida

Y este es el buen caso cuando se documenta el error.

Todos 135 comentarios

Solo para aclarar: una de las ideas aquí no es obligar a los usuarios a detectar la excepción, sino más bien, ¿inferir mejor el tipo de una variable de cláusula de captura?

@DanielRosenwasser
Sí, los usuarios no se verán obligados a detectar excepciones, por lo que está bien con el compilador (en tiempo de ejecución, por supuesto, se produce el error):

function fn() {
    throw "error";
}

fn();

// and
try {
    fn();
} finally {
    // do something here
}

Pero les dará a los desarrolladores una forma de expresar qué excepciones se pueden lanzar (sería increíble tener eso cuando se usan otros archivos .d.ts bibliotecas) y luego hacer que el tipo de compilador guarde los tipos de excepción dentro de la cláusula catch.

¿En qué se diferencia un tiro marcado de Tried<Result, Error> ?

type Tried<Result, Error> = Success<Result> | Failure<Error>;
interface Success<Result> { kind: 'result', result: Result } 
interface Failure<Error> { kind: 'failure', error: Error }
function isSuccess(tried: Tried<Result, Error>): tried is Success<Result> {
   return tried.kind === 'result';
}
function mightFail(): Tried<number, string> {
}
const tried = mightFail();
if (isSuccess(tried)) {
    console.log(tried.success);
}  else {
    console.error(tried.error);
}

en vez de

try {
    const result: Result = mightFail();
    console.log(success);
} catch (error: Error) {
    console.error(error);
}

@aleksey-bykov

Estás sugiriendo no usar throw en absoluto en mi código y en su lugar envolver los resultados (en funciones que pueden generar errores).
Este enfoque tiene algunos inconvenientes:

  • Este envoltorio crea más código.
  • Requiere que toda la cadena de función invocada devuelva este valor envuelto (o error) o, alternativamente, la función que obtiene Tried<> no puede elegir ignorar el error.
  • No es un estándar, las bibliotecas de terceros y los js nativos arrojan errores

Agregar throws permitirá a los desarrolladores que elijan manejar errores de su código, bibliotecas 3rd y js nativo.
Como la sugerencia también solicita la inferencia de errores, todos los archivos de definición generados pueden incluir la cláusula throws .
Será muy conveniente saber qué errores puede arrojar una función directamente desde el archivo de definición en lugar del estado actual donde debe ir a los documentos, por ejemplo, para saber qué error JSON.parse podría arrojar. a la página de MDN y lea eso:

Lanza una excepción SyntaxError si la cadena a analizar no es JSON válida

Y este es el buen caso cuando se documenta el error.

Y este es el buen caso cuando se documenta el error.

¿Hay una forma confiable en javascript para distinguir SyntaxError de Error?

  • sí, es más código, pero dado que una mala situación se representa en un objeto, se puede pasar para que se procese, descarte, almacene o transforme en un resultado válido como cualquier otro valor

  • puede ignorar el intento devolviendo el intento también, el intento se puede ver como una mónada, busque cálculos monádicos

    function mightFail(): Tried<number, string> {
    }
    function mightFailToo(): Tried<number, string> {
        const tried = mightFail();
        if (isSuccess(tried))  { 
             return successFrom(tried.result * 2);
        } else {
             return tried;
        }
    }
    
  • es lo suficientemente estándar para su código, cuando se trata de bibliotecas de terceros que lanzan una excepción, generalmente significa un fin de juego para usted, porque es casi imposible recuperarse de manera confiable de una excepción, la razón es que se puede lanzar desde cualquier lugar dentro del código terminarlo en una posición arbitraria y dejar su estado interno incompleto o corrupto

  • no hay soporte para excepciones verificadas del tiempo de ejecución de JavaScript, y me temo que no se puede implementar solo en mecanografiado

aparte de eso, codificar una excepción como un caso de resultado especial es una práctica muy común en el mundo de FP

mientras que dividir un posible resultado en 2 partes:

  • uno entregado por la declaración de devolución y
  • otro entregado por tiro

parece una dificultad inventada

en mi opinión, throw es bueno para fallar rápido y fuerte cuando no puedes hacer nada al respecto, los resultados codificados explícitamente son buenos para cualquier cosa que implique una situación mala pero esperada de la que puedas recuperarte

considerar:

// throw/catch
declare function doThis(): number throws string;
declare function doThat(): number throws string;
function doSomething(): number throws string {
    let oneResult: number | undefined = undefined;
    try {
        oneResult = doThis();
    } catch (e) {
        throw e;
    }

    let anotherResult: number | undefined = undefined;
    try {
        anotherResult = doThat();
    } catch (e) {
        throw e;
    }
    return oneResult + anotherResult;
}

// explicit results
declare function doThis(): Tried<number, string>;
declare function doThat(): Tried<number, string>;
function withBothTried<T, E, R>(one: Tried<T, E>, another: Tried<T, E>, haveBoth: (one: T, another: T) => R): Tried<T, R> {
    return isSuccess(one)
        ? isSuccess(another)
            ? successFrom(haveBoth(one.result, another.result))
            : another
        : one;
}
function add(one: number, another: number) { return one + another; }
function doSomething(): Tried<number, string> {
    return withBothTried(
        doThis(),
        doThat(),
        add
    );
}

@aleksey-bykov

Mi punto con JSON.parse podría arrojar SyntaxError es que necesito buscar la función en los documentos solo para saber que podría arrojar, y sería más fácil verlo en el .d.ts .
Y sí, puedes saber que es SyntaxError con el uso instanceof .

Puedes representar la misma mala situación lanzando un error.
Puede crear su propia clase de error que extienda Error y poner todos los datos relevantes que necesita en ella.
Obtienes lo mismo con menos código.

A veces, tiene una larga cadena de invocaciones de funciones y es posible que desee tratar algunos de los errores en diferentes niveles de la cadena.
Será bastante molesto usar siempre resultados envueltos (mónadas).
Sin mencionar que, de nuevo, otras bibliotecas y errores nativos podrían arrojarse de todos modos, por lo que podría terminar usando tanto monads como try/catch.

No estoy de acuerdo contigo, en muchos casos puedes recuperarte de los errores arrojados, y si el idioma te permite expresarlo mejor, será más fácil hacerlo.

Al igual que con muchas cosas en mecanografiado, la falta de soporte de la función en javascript no es un problema.
Esta:

try {
    mightFail();
} catch (e: MyError | string) {
    if (e instanceof MyError) { ... }
    else if (typeof e === "string") { ... }
    else {}
}

Funcionará como se esperaba en javascript, solo que sin la anotación de tipo.

Usar throw es suficiente para expresar lo que está diciendo: si la operación tuvo éxito, devuelva el valor; de lo contrario, arrojará un error.
El usuario de esta función decidirá entonces si quiere tratar los posibles errores o ignorarlos.
Puede lidiar solo con los errores que usted mismo arrojó e ignorar los que son de terceros, por ejemplo.

si hablamos de navegadores instanceof solo es bueno para cosas que se originan en la misma ventana/documento, pruébelo:

var child = window.open('about:blank');
console.log(child.Error === window.Error);

así que cuando lo hagas:

try { child.doSomething(); } catch (e) { if (e instanceof SyntaxError) { } }

no lo atraparás

otro problema con las excepciones que podrían deslizarse en su código desde mucho más allá de donde espera que sucedan

try {
   doSomething(); // <-- uses 3rd party library that by coincidence throws SyntaxError too, but you don' t know it 
} catch (e) {}

además instanceof es vulnerable a la herencia de prototipos, por lo que debe tener precauciones adicionales para verificar siempre contra el ancestro final

class StandardError {}
class CustomError extends StandardError {
}
function doSomething() { throw new CustomError(); }
function oldCode() {
   try {
      doSomething();
   } catch (e) {
      if (e instanceof StandardError) {
          // problem
      }
   }
}

@ aleksey-bykov Enhebrar explícitamente errores como sugieres en estructuras monádicas es una tarea bastante difícil y desalentadora. Requiere mucho esfuerzo, hace que el código sea difícil de entender y requiere soporte de lenguaje/emisión basada en tipos para estar al borde de ser soportable. Este es un comentario de alguien que pone mucho esfuerzo en popularizar Haskell y FP como un todo.

Es una alternativa de trabajo, especialmente para los entusiastas (incluido yo mismo), sin embargo, no creo que sea una opción viable para el público en general.

En realidad, mi principal preocupación aquí es que las personas comenzarán a subclasificar Error. Creo que este es un patrón terrible. De manera más general, cualquier cosa que promueva el uso del operador instanceof solo creará confusión adicional en torno a las clases.

Este es un comentario de alguien que pone mucho esfuerzo en popularizar Haskell y FP como un todo.

Realmente creo que esto debería impulsarse más a la audiencia, hasta que no se digiera y se pida más, no podremos tener un mejor soporte de FP en el idioma.

y no es tan desalentador como cree, siempre que todos los combinadores ya estén escritos, solo utilícelos para crear un flujo de datos, como lo hacemos en nuestro proyecto, pero estoy de acuerdo en que TS podría haberlo respaldado mejor: # 2319

Los transformadores Monad son una verdadera PITA. Necesita elevación, elevación y funcionamiento selectivo con bastante frecuencia. El resultado final es un código difícilmente comprensible y una barrera de entrada mucho más alta que la necesaria. Todos los combinadores y funciones de elevación (que proporcionan el encajonado/desencajonado obligatorio) son solo ruido que lo distrae del problema en cuestión. Creo que ser explícito sobre el estado, los efectos, etc. es algo bueno, pero no creo que hayamos encontrado una envoltura/abstracción conveniente todavía. Hasta que lo encontremos, admitir patrones de programación tradicionales parece ser el camino a seguir sin detenerse a experimentar y explorar mientras tanto.

PD: Creo que necesitamos más que operadores personalizados. Los tipos superiores y algún tipo de clase también son esenciales para una biblioteca monádica práctica. Entre ellos, calificaría a HKT en primer lugar y a las clases de tipo en segundo lugar. Dicho todo esto, creo que TypeScript no es el lenguaje para practicar tales conceptos. Jugando, sí, pero su filosofía y raíces son fundamentalmente distantes para una integración perfecta y adecuada.

Volviendo a la pregunta OP: instanceof es un operador peligroso de usar. Sin embargo, las excepciones explícitas no se limitan a Error . También puede lanzar sus propios ADT o errores POJO personalizados. La característica propuesta puede ser bastante útil y, por supuesto, también puede ser mal utilizada. En cualquier caso, hace que las funciones sean más transparentes, lo que sin duda es algo bueno. En general, estoy 50/50 en eso :)

@aleksey-bykov

Los desarrolladores deben tener en cuenta los diferentes problemas de js que describió, después de todo, agregar throws a mecanografiado no introduce nada nuevo en js, solo le da a mecanografiado como lenguaje la capacidad de expresar un comportamiento js existente.

El hecho de que las bibliotecas de terceros puedan arrojar errores es exactamente mi punto.
Si sus archivos de definición incluyeran eso, tendré una forma de saberlo.

@aluanhaddad
¿Por qué es un patrón terrible extender Error ?

@gcnew
En cuanto a instanceof , ese fue solo un ejemplo, siempre puedo arrojar objetos regulares que tienen diferentes tipos y luego usar protectores de tipo para diferenciarlos.
Dependerá del desarrollador decidir qué tipo de errores desea arrojar, y probablemente ya sea el caso, pero actualmente no hay forma de expresar eso, que es lo que esta sugerencia quiere resolver.

@nitzantomer La subclasificación de clases nativas ( Error , Array , RegExp , etc.) no se admitía en versiones anteriores de ECMAScript (anteriores a ES6). La emisión de nivel bajo para estas clases da resultados inesperados (se hace el mejor esfuerzo, pero esto es lo más lejos que se puede llegar) y es la razón de numerosos problemas registrados a diario. Como regla general, no subclasifique nativos a menos que esté apuntando a versiones recientes de ECMAScript y realmente sepa lo que está haciendo.

@gcnew
Oh, soy muy consciente de eso ya que pasé más de unas pocas horas tratando de averiguar qué salió mal.
Pero con la capacidad de hacerlo ahora, no debería haber una razón para no hacerlo (al apuntar a es6).

En cualquier caso, esta sugerencia no asume que el usuario está subclasificando la clase Error, solo fue un ejemplo.

@nitzantomer No estoy argumentando que la sugerencia se limite a Error . Acabo de explicar por qué es un mal patrón subclasificarlo. En mi publicación, de hecho, defendí la postura de que también se pueden usar objetos personalizados o uniones discriminadas.

instanceof es peligroso y se considera un antipatrón incluso si elimina las especificidades de JavaScript, por ejemplo, tenga cuidado con el operador instanceof . La razón es que el compilador no puede protegerlo contra errores introducidos por nuevas subclases. La lógica que usa instanceof es frágil y no sigue el principio abierto/cerrado , ya que solo espera un puñado de opciones. Incluso si se agrega un caso comodín, es probable que los nuevos derivados causen errores, ya que pueden romper las suposiciones hechas en el momento de escribir este artículo.

Para los casos en los que desea distinguir entre alternativas conocidas, TypeScript tiene uniones etiquetadas (también llamadas uniones discriminadas o tipos de datos algebraicos). El compilador se asegura de que se manejen todos los casos, lo que le brinda buenas garantías. La desventaja es que si desea agregar una nueva entrada al tipo, tendrá que revisar todo el código que lo discrimina y manejar la opción recién agregada. La ventaja es que dicho código probablemente se habría roto, pero habría fallado en el tiempo de ejecución.

Pensé dos veces en esta propuesta y me puse en contra. La razón es que si las declaraciones throws estaban presentes en las firmas pero no se aplicaron, ya pueden manejarse mediante comentarios de documentación. En el caso de que se apliquen, comparto el sentimiento de que se volverían irritantes y se tragarían rápido ya que JavaScript carece del mecanismo de Java para las cláusulas de captura escritas. El uso de excepciones (especialmente como flujo de control) nunca ha sido una práctica establecida. Todo esto me lleva a comprender que las excepciones verificadas aportan muy poco, mientras que hay formas mejores y más comunes de representar el fracaso (por ejemplo, el retorno de la unión).

@gcnew
Así es como se hace en C#, el problema es que los documentos no son estándar en TypeScript.
No recuerdo haberme encontrado con un archivo de definición que esté bien documentado. Los diferentes archivos lib.d.ts contienen comentarios, pero no contienen errores (con una excepción: lib.es6.d.ts tiene un throws en Date[Symbol.toPrimitive](hint: string) ).

Además, esta sugerencia tiene en cuenta la inferencia de errores, algo que no sucederá si los errores provienen de los comentarios de la documentación. Con las excepciones verificadas inferidas, el desarrollador ni siquiera necesitará especificar la cláusula throws , el compilador la inferirá automáticamente y la usará para la compilación y la agregará al archivo de definición resultante.

Estoy de acuerdo en que hacer cumplir el manejo de errores no es algo bueno, pero tener esta función solo agregará más información que luego pueden usar aquellos que lo deseen.
El problema con:

... hay formas mejores y actualmente más comunes de representar el fracaso

Es que no hay una forma estándar de hacerlo.
Puede usar union return, @aleksey-bykov usará Tried<> y un desarrollador de otra biblioteca de terceros hará algo completamente diferente.
Lanzar errores es un estándar en todos los lenguajes (js, java, c#...) y como es parte del sistema y no una solución alternativa, debería (en mi opinión) tener un mejor manejo en mecanografiado, y prueba de ello es el número de problemas que he visto aquí a lo largo del tiempo que solicitan una anotación de tipo en la cláusula catch .

Me encantaría tener información en la información sobre herramientas en VS si una función (o función llamada) puede arrojar. Para archivos *.d.ts probablemente necesitemos un parámetro falso como este desde TS2.0.

@HolgerJeromin
¿Por qué sería necesario?

aquí hay una pregunta simple, ¿qué firma se debe inferir para dontCare en el código a continuación?

function mightThrow(): void throws string {
   if (Math.random() > 0.5) {
       throw 'hey!';
   }
}

function dontCare() {
   return mightThrow();
}

de acuerdo a lo que dijiste en tu propuesta debería ser

function dontCare(): void throws string {

digo que debería ser un error de tipo ya que una excepción marcada no se manejó correctamente

function dontCare() { // <-- Checked exception wasn't handled.
         ^^^^^^^^^^

¿porqué es eso?

porque de lo contrario hay una muy buena posibilidad de corromper el estado de la persona que llama inmediatamente:

class MyClass {
    private values: number[] = [];

    keepAllValues(values: number[]) {
       for (let index = 0; index < values.length; index ++) {
            this.values.push(values[index]); 
            mightThrow();
       }
    }
}

si deja pasar una excepción, no puede inferirla como verificada, porque el contrato de comportamiento de keepAllValues se violaría de esta manera (no todos los valores se mantuvieron a pesar de la intención original)

la única forma segura de hacerlo es atraparlos inmediatamente y volver a lanzarlos explícitamente

    keepAllValues(values: number[]) {
           for (let index = 0; index < values.length; index ++) {
                this.values.push(values[index]); 
                try {
                    mightThrow();
                } catch (e) {
                    // the state of MyClass is going to be corrupt anyway
                    // but unlike the other example this is a deliberate choice
                    throw e;
                }
           }
    }

de lo contrario, a pesar de que las personas que llaman saben lo que se puede arrojar, no puede darles garantías de que es seguro continuar usando el código que acaba de arrojar

por lo tanto , no existe tal cosa como la propagación automática del contrato de excepción verificada

y corrígeme si me equivoco, esto es exactamente lo que hace Java, que mencionaste como ejemplo anteriormente

@aleksey-bykov
Esta:

function mightThrow(): void {
   if (Math.random() > 0.5) {
       throw 'hey!';
   }
}

function dontCare() {
   return mightThrow();
}

Significa que tanto mightThrow como dontCare se infieren a throws string , sin embargo:

function dontCare() {
    try {
        return mightThrow();
    } catch (e: string) {
        // do something
    }
}

No tendrá una cláusula throw porque se manejó el error.
Esta:

function mightThrow(): void throws string | MyErrorType { ... }

function dontCare() {
    try {
        return mightThrow();
    } catch (e: string | MyErrorType) {
        if (typeof e === "string") {
            // do something
        } else { throw e }
    }
}

Tendrá throws MyErrorType .

En cuanto a su ejemplo keepAllValues , no estoy seguro de lo que quiere decir en su ejemplo:

class MyClass {
    private values: number[] = [];

    keepAllValues(values: number[]) {
       for (let index = 0; index < values.length; index ++) {
            this.values.push(values[index]); 
            mightThrow();
       }
    }
}

MyClass.keepAllValues se deducirá como throws string porque mightThrow podría arrojar un string y ese error no se manejó.

En cuanto a su ejemplo keepAllValues , no estoy seguro de lo que quiere decir

Quise decir que las excepciones que vienen sin manejar de mightThrow interrumpen keepAllValues y hacen que termine en medio de lo que estaba haciendo dejando su estado corrupto. Es un problema. Lo que sugieres es cerrar los ojos ante este problema y fingir que no es grave. Lo que sugiero es abordar este problema al exigir que todas las excepciones verificadas se manejen de inmediato y se vuelvan a generar explícitamente. De esta manera no hay forma de corromper al estado sin querer . Y aunque aún puede estar corrupto si así lo desea, requeriría una codificación deliberada.

Piénselo, hay 2 formas en que podemos abordar las excepciones:

  • suéltelos, lo que conduce a un bloqueo, si el bloqueo es lo que quiere, entonces estamos bien aquí
  • si no desea un bloqueo, entonces necesita alguna orientación sobre qué tipo de excepción debe buscar, y aquí es donde entra su propuesta: excepciones marcadas, todas enumeradas explícitamente, para que pueda manejarlas todas y no no te pierdas nada

ahora, si decidimos ir con las excepciones marcadas que se manejan correctamente y evitan un bloqueo, debemos descartar una situación en la que manejamos una excepción que proviene de varias capas de profundidad de donde la está capturando:

export function calculateFormula(input) {
    return calculateSubFormula(input);
}
export function calculateSubFormula(input) {
   return calculateSubSubFormula(input);
}
export function calculateSubSubFormula(input): number throws DivisionByZero  {
   return 1/input;
}

try {
   calculateFormula(0);
} catch (e: DivisionByZero) {
   // it doesn't make sense to expose DivisionByZero from under several layers of calculations
   // to the top level where nothing we can do or even know what to do about it
   // basically we cannot recover from it, because it happened outside of our immediate reach that we can control
}

el ejemplo anterior trae un caso interesante para su consideración, cuál sería la firma inferida de:

function boom(value: number) /* what comes here?*/  {
    return 1/value;
}

otro caso interesante

// 1.
function run<R, E>(callback(): R throws E) /* what comes here? */ {
    try {
        return callback();
    } catch (e: DivisionByZero) {
        // ignore
    }
}

function throw() { return 1 / 0; }

// 2.
run(throw); /* what do we expect here? */


@aleksey-bykov
Entonces, ¿propone que todos los errores deben manejarse como lo es con Java?
No soy fanático de eso (aunque vengo de Java y todavía me encanta) porque js/ts son mucho más dinámicos y sus usuarios están acostumbrados a eso.
Puede ser un indicador que te hace lidiar con errores si lo incluyes al compilar (como strictNullChecks ).

Mi sugerencia no está aquí para resolver excepciones no controladas, el código que publicó se romperá ahora sin que se implemente esta característica, y también se rompería en js.
Mi sugerencia es permitirle a usted, como desarrollador, ser más consciente de los diferentes errores que pueden generarse, aún depende de usted si manejarlos o ignorarlos.

En cuanto al problema de la división por 0, no da como resultado un error:

console.log(1 / 0) // Infinity
console.log(1 / "hey!") // NaN

más consciente de los diferentes errores que se pueden producir

no tiene sentido hacerlo a menos que puedan lidiar con ellos, la propuesta actual no es viable debido a los casos que enumeré

Entonces, ¿propone que todos los errores deben manejarse como lo es con Java?

sí, esto es lo que significa haber verificado las excepciones

@aleksey-bykov
No veo por qué ninguno de los casos que enumeraste hace que esta propuesta sea inviable.

No hay problema con el manejo de un error que se arrojó en la cadena de invocación, incluso si estoy usando una función que se dedujo de arrojar DivisionByZero (independientemente de dónde se arrojó), puedo elegir manejarlo .
Puedo intentar volver a intentarlo con diferentes argumentos, puedo mostrarle al usuario un mensaje de que algo salió mal, puedo registrar este problema para que luego pueda cambiar mi código para manejarlo (si sucede con frecuencia).

Una vez más, esta propuesta no cambia nada en tiempo de ejecución, por lo que todo lo que funcionó seguirá funcionando como antes.
La única diferencia es que tendré más información sobre los errores que pueden arrojarse.

Veo lo que está diciendo, no se cambiará nada en el tiempo de ejecución de JavaScript, sin embargo, su mensaje aquí es para dar a los usuarios la ilusión de que saben lo que están haciendo al manejar una excepción que vino de 20 capas por debajo con la misma confianza que manejarían una excepción inmediata

simplemente no hay forma de que puedan solucionar un problema que ocurrió 20 capas más abajo

puede registrarlo, seguro, como cualquier excepción no verificada, pero no puede arreglarlo

entonces es mentira en general, ya hay bastantes mentiras en TS, no confundamos mas a la gente

@aleksey-bykov
Lo que está describiendo existe en todos los idiomas que admiten excepciones.
Nadie dijo que capturar una excepción solucionaría el problema, pero le permitirá manejarlo con gracia.

Saber qué errores se pueden generar al invocar una función ayudará a los desarrolladores a separar los errores que pueden manejar y los que no.

En este momento, es posible que los desarrolladores no sepan que el uso JSON.parse podría arrojar un error, pero si fuera parte del lib.d.ts y el IDE se lo hiciera saber (por ejemplo), entonces tal vez elija manejar este caso.

no puede manejar correctamente un problema que ocurrió 20 capas más abajo, porque el estado interno está corrupto en 19 capas y no puede ir allí porque el estado es privado

para ser constructivo: lo que sugiero es exigir a los usuarios que manejen las excepciones marcadas de inmediato y volver a lanzarlas explícitamente, de esta manera descartamos confusiones no intencionadas y separamos las excepciones marcadas de las no marcadas:

  • excepción comprobada: sucedió en el alcance inmediato y debe manejarse, de esta manera se garantiza que el estado no es corrupto y es seguro continuar
  • excepción no verificada: sucedió en el alcance inmediato o mucho más abajo, no se puede manejar porque el estado estaba dañado, puede iniciar sesión o continuar bajo su propio riesgo

SyntaxError en JSON.parse debe declararse como una excepción comprobada

@aleksey-bykov

No veo por qué es necesario obligar a los desarrolladores a hacer algo que no desean, algo que no han hecho hasta ahora.

Aquí hay un ejemplo:
Tengo un cliente web en el que el usuario puede escribir/pegar datos json y luego hacer clic en un botón.
La aplicación toma esta entrada y la pasa a una biblioteca de terceros que de alguna manera analiza este json y lo devuelve junto con los diferentes tipos de valores (cadena, número, booleano, matriz, etc.).
Si esta biblioteca de terceros arroja un SyntaxError que puedo recuperar: informe al usuario que su entrada no es válida y que debe intentarlo de nuevo.

Al saber qué errores se pueden generar al invocar una función, el desarrollador puede decidir qué puede/desea manejar y qué no.
No debería importar qué tan profundo en la cadena se arrojó el error.

mira, parece que no entiendes lo que digo, vamos en círculos

al permitir que SyntaxError se arrojen desde la biblioteca de terceros, está exponiendo a su usuario a los detalles de implementación de su propio código que se supone que debe estar encapsulado

básicamente estás diciendo, hey, no es mi código el que no funciona, es esa biblioteca estúpida que encontré en Internet y usé, así que si tienes un problema con eso, trata con esa biblioteca de terceros, no yo, yo solo dije lo que me pidieron

y no hay garantía de que aún pueda usar la instancia de esa 3rd lib después de ese SyntaxError, es su responsabilidad proporcionar garantías al usuario, por ejemplo, reinstaurando el control de terceros después de que arrojó

En pocas palabras, debe estar a cargo del manejo de las excepciones internas ( no todas, solo las marcadas, se lo ruego )

Entiendo lo que dices, pero no estoy de acuerdo.
Tienes razón, eso es básicamente lo que estoy diciendo.
Si usé una biblioteca de terceros que arroja un error, puedo elegir tratarlo o ignorarlo y dejar que el usuario de mi código lo maneje.
Hay muchas razones para hacerlo, por ejemplo, la lib que estoy escribiendo es independiente de la interfaz de usuario, por lo que no puedo informar al usuario que algo anda mal, pero quien alguna vez use mi lib puede manejar los errores que se producen al usar my lib y manejarlos interactuando con el usuario.

Si una biblioteca se queda con un estado corrupto cuando se lanza, entonces probablemente necesite documentarlo.
Si luego uso una biblioteca de este tipo y, como resultado, se produce un error, mi estado se corrompe, entonces necesito documentarlo.

Línea de fondo:
Esta sugerencia viene a ofrecer más información sobre los errores lanzados.
No debería obligar a los desarrolladores a hacer las cosas de manera diferente, solo facilitarles la tarea de lidiar con los errores si así lo desean.

puede estar en desacuerdo, está bien, simplemente no las llamemos excepciones comprobadas, por favor, porque la forma en que lo dice no es lo que son las excepciones comprobadas

llamémoslas excepciones enumeradas o reveladas , porque lo único que importa es que los desarrolladores las conozcan

@aleksey-bykov
Bastante justo, el nombre cambió.

@aleksey-bykov

no puede manejar correctamente un problema que ocurrió 20 capas más abajo, porque el estado interno está corrupto en 19 capas y no puede ir allí porque el estado es privado

No, no puede arreglar el estado interno, pero ciertamente podría arreglar el estado local, y ese es exactamente el punto de manejarlo aquí y no más abajo en la pila.

Si su argumento es que no hay forma de estar seguro en qué estado se encuentran algunos valores mutables compartidos al manejar la excepción, entonces es un argumento en contra de la programación imperativa y no se limita a esta propuesta.

si cada capa está obligada a asumir la responsabilidad de reaccionar ante una excepción que proviene inmediatamente de una capa inferior, hay muchas más posibilidades de una recuperación exitosa, esta es la idea detrás de las excepciones marcadas como yo lo veo

para decirlo en otras palabras, las excepciones que provienen de más de 1 nivel por debajo es una oración, es demasiado tarde para hacer otra cosa que no sea volver a crear una instancia de toda la infraestructura desde cero (si tiene la suerte, no hay sobras globales que pueda ' t alcanzar)

la propuesta como se indica es en su mayoría inútil, porque no hay una forma confiable de reaccionar ante el conocimiento de algo malo que sucedió fuera de su alcance

Esto es genial. FWIW: Creo que si se agrega, debería ser necesario de forma predeterminada para manejar los métodos de lanzamiento o marcar su método como lanzamiento también. De lo contrario, es solo documentación más o menos.

@agonzalezjr
Creo que, como la mayoría de las funciones en mecanografiado, también debería poder optar por esta función.
Al igual que no es obligatorio agregar tipos, no debería ser obligatorio lanzar/atrapar.

Probablemente debería haber una bandera para que sea imprescindible, como --onlyCheckedExceptions .

En cualquier caso, esta función también se utilizará para inferir/validar los tipos de excepciones lanzadas, por lo que no solo para la documentación.

@nitzantomer

Aquí hay un ejemplo:
Tengo un cliente web en el que el usuario puede escribir/pegar datos json y luego hacer clic en un botón.
La aplicación toma esta entrada y la pasa a una biblioteca de terceros que de alguna manera analiza este json y lo devuelve junto con los diferentes tipos de valores (cadena, número, booleano, matriz, etc.).
Si esta biblioteca de terceros arroja un SyntaxError, puedo recuperarlo: informe al usuario que su entrada no es válida y que debe intentarlo de nuevo.

Esta es ciertamente un área donde la idea de las excepciones comprobadas se vuelve turbia. También es donde la definición de _situación excepcional_ se torna confusa.
El programa en su ejemplo sería un argumento para que JSON.parse se declare como una excepción comprobada.
Pero, ¿qué pasa si el programa es un cliente HTTP y está llamando a JSON.parse según el valor de un encabezado adjunto a una respuesta HTTP que contiene un cuerpo mal formado? No hay nada significativo que el programa pueda hacer para recuperarse, todo lo que puede hacer es volver a lanzar.
Diría que este es un argumento en contra de que JSON.parse se declare como verificado.

Todo depende del caso de uso.

Entiendo que está proponiendo que esto esté bajo una bandera, pero imaginemos que quiero usar esta función, así que he habilitado la bandera. Dependiendo del tipo de programa que esté escribiendo, puede ayudarme o dificultarme.

Incluso la clásica java.io.FileNotFoundException es un ejemplo de esto. Está comprobado, pero ¿puede recuperarse el programa? Realmente depende de lo que signifique el archivo que falta para la persona que llama, no para el destinatario.

@aluanhaddad

Esta sugerencia no propone agregar ninguna funcionalidad nueva, solo agregar una forma de expresar en mecanografiado algo que ya existe en javascript.
Se lanzan errores, pero actualmente el mecanografiado no tiene forma de declararlos (al lanzar o atrapar).

En cuanto a su ejemplo, al detectar el error, el programa puede fallar "graciosamente" (por ejemplo, mostrando al usuario un mensaje de "algo salió mal") al detectar este error, o puede ignorarlo, según el programa/desarrollador.
Si el estado de los programas puede verse afectado por este error, manejarlo puede mantener un estado válido en lugar de uno roto.

En cualquier caso, el desarrollador debe tomar la decisión de si puede recuperarse de un error lanzado o no.
También depende de él decidir qué significa recuperar, por ejemplo, si estoy escribiendo este cliente http para usarlo como una biblioteca de terceros, es posible que desee que todos los errores arrojados desde mi biblioteca sean del mismo tipo:

enum ErrorCode {
    IllFormedJsonResponse,
    ...
}
...
{
    code: ErrorCode;
    message: string;
}

Ahora, en mi biblioteca, cuando analizo la respuesta usando JSON.parse , quiero detectar un error arrojado y luego arrojar mi propio error:

{
    code: ErrorCode.IllFormedJsonResponse,
    message: "Failed parsing response"
} 

Si se implementa esta función, me resultará fácil declarar este comportamiento y los usuarios de mi biblioteca tendrán claro cómo funciona y cómo falla.

Esta sugerencia no propone agregar ninguna funcionalidad nueva, solo agregar una forma de expresar en mecanografiado algo que ya existe en javascript.

Sé. Estoy hablando de los errores que TypeScript emitiría bajo esta propuesta.
Mi suposición fue que esta propuesta implicaba una distinción entre especificadores de excepción verificados y no verificados (inferidos o explícitos), nuevamente solo para fines de verificación de tipos.

@aluanhaddad

Lo que dijiste en el comentario anterior:

Pero, ¿qué sucede si el programa es un cliente HTTP y está llamando a JSON.parse según el valor de un encabezado adjunto a una respuesta HTTP que contiene un cuerpo mal formado? No hay nada significativo que el programa pueda hacer para recuperarse, todo lo que puede hacer es volver a lanzar.

Se aplica lo mismo para devolver un null cuando mi función se declara para devolver un resultado.
Si el desarrollador elige usar strictNullChecks entonces puede decir exactamente lo mismo si la función devuelve null (en lugar de arrojar) entonces en el mismo escenario "no hay nada significativo que el programa pueda hacer para recuperar".

Pero incluso sin usar un indicador onlyCheckedExceptions , esta función sigue siendo útil porque el compilador se quejará, por ejemplo, si trato de detectar el error como string cuando se declara que la función arroja solo Error .

Buena idea, sería útil pero no estricto / tipo seguro ya que no hay forma de saber qué llamadas anidadas podrían arrojarle.

Es decir, si tengo una función que podría arrojar una excepción de tipo A, pero adentro llamo a una función anidada y no la pongo en try catch, arrojará su excepción de tipo B a la persona que llama.
Por lo tanto, si la persona que llama espera solo excepciones de tipo A, no hay garantía de que no obtendrá otros tipos de excepciones anidadas.

(el hilo es demasiado largo, lo siento si me perdí este comentario)

@shaipetel
La proposición establece que el compilador inferirá los tipos de errores no controlados y los agregará a la firma de la función/método.
Entonces, en el caso que describió, su función arrojará A | B en caso de que no se haya manejado B .

Oh ya veo. ¿Profundizará en todos los métodos a los que llamo y recopilará todos los tipos de excepciones posibles?
Me encantaría ver que suceda, si es posible. Vea, un desarrollador siempre puede tener una excepción inesperada que no se declarará, en cuyo caso un "objeto no establecido en instancia" o "dividir por 0" o excepciones similares siempre son posibles casi desde cualquier función.
En mi humilde opinión, se habría manejado mejor como en C #, donde todas las excepciones se heredan de una clase base que tiene un mensaje y no permitir en absoluto el lanzamiento de texto sin envolver u otros objetos. Si tiene una clase base y herencia, puede conectar en cascada sus capturas y manejar su error esperado en un bloque y otro inesperado en otro.

@shaipetel
En javascript, todos los errores se basan en la clase Error , pero no está restringido a arrojar errores, puede arrojar cualquier cosa:

  • throw "something went wrong"
  • throw 0
  • throw { message: "something went wrong", code: 4 }

Sí, sé cómo funciona JavaScript, estamos hablando de TypeScript, que impone más limitaciones.
Sugerí, una buena solución en mi humilde opinión sería hacer que TypeScript siga el manejo de excepciones que requiere que todas las excepciones lanzadas sean de una clase base específica y no permitan arrojar valores sin envolver directamente.

Por lo tanto, no permitirá "arrojar 0" o "arrojar 'algún error'".
Al igual que JavaScript permite muchas cosas que TypeScript no permite.

Gracias,

@nitzantomer

Se aplica lo mismo para devolver un valor nulo cuando mi función se declara para devolver un resultado.
Si el desarrollador elige usar strictNullChecks, entonces puede decir exactamente lo mismo si la función devuelve un valor nulo (en lugar de arrojarlo), entonces en el mismo escenario "no hay nada significativo que el programa pueda hacer para recuperarse".

Pero incluso sin usar un indicador onlyCheckedExceptions, esta función sigue siendo útil porque el compilador se quejará, por ejemplo, si trato de detectar el error como una cadena cuando se declara que la función arroja solo Error.

Veo lo que dices, tiene sentido.

@shaipetel como se discutió anteriormente en esta propuesta y en otros lugares, la subclasificación de funciones integradas como Error y Array no funciona. Conduce a un comportamiento que es diferente en tiempos de ejecución y objetivos de compilación ampliamente utilizados.

La captura de valores de varios tipos que no son de error es la forma más viable de visualizar la funcionalidad propuesta. En realidad, no lo veo como un problema, ya que los mecanismos de propagación de excepciones que aprovechan esta función probablemente generarían errores que son mucho más específicos y mucho más útiles que, por ejemplo, un seguimiento de la pila u otras propiedades específicas del error.
Extender Error no es viable. No será viable hasta que todos los objetivos por debajo de es2015 ya no estén en uso.

Si esta propuesta conduce indirectamente a más subclases de la función Error , creo que es una mala idea. Tal objeción está completamente separada de cualquier objeción filosófica sobre el uso de excepciones para controlar el flujo o la definición de circunstancia excepcional. Por lo tanto, si se adoptara esta propuesta, esperaría una documentación extremadamente ruidosa y directa sobre el uso correcto y la necesidad de evitar la subclasificación Error .

Me encantaría que hubiera algún tipo de lógica de ayuda para manejar las excepciones escritas, estoy refactorizando una gran cantidad de código de promesa para usar async/await, que actualmente se ve así:

doSomethingWhichReturnsPromise()
    .then(send(200))
    .catch(NotFoundError, (error) => { send(404); })
    .catch(SomeBadDataError, (error) => { send(400); })
    .catch(CantSeeThisError, (error) => { send(403); })
    .catch((error) => { send(500); })

Luego, en el nuevo mundo se ve así:

{
    await doSomethingWhichReturnsPromise();
    send(200);
}
catch(error)
{
    if(error instanceof NotFoundError) { send(404); } 
    else if(error instanceof SomeBadDataError) { send(400); } 
    else if(error instanceof CantSeeThisError) { send(403); } 
    else { send(500); } 
}

Lo cual está bien, pero requiere más código y es un poco menos legible en algunos aspectos, por lo que sería genial si hubiera algún tipo de soporte para:

{
    await doSomethingWhichReturnsPromise();
    send(200);
}
catch(NotFoundError, error) { send(404); }
catch(SomeBadDataError, error) { send(404); }
catch(CantSeeThisError, error) { send(404); }
catch(error) { send(404); }

Lo que generaría el bit anterior, pero como azúcar sintáctico, incluso podría hacerlo como genérico, pero es un poco más desagradable:

{
    await doSomethingWhichReturnsPromise();
    send(200);
}
catch<NotFoundError>(error) { send(404); }
catch<SomeBadDataError>(error) { send(404); }
catch<CantSeeThisError>(error) { send(404); }
catch(error) { send(404); }

@grofit
aunque me gustaría que mecanografiado respaldara lo que sugiere, en mi opinión, no está relacionado con este problema.
Lo que está sugiriendo puede incluso implementarse sin implementar de qué se trata este problema, solo que el compilador no podrá quejarse (por ejemplo) de que NotFoundError no se arroja.

Pensé que esto era una sugerencia para capturas mecanografiadas, no un problema de ningún tipo, simplemente no quería duplicar hilos, lo publicaré en su propio número entonces.

@grofit
Las capturas tipeadas también son parte de la solicitud de función, pero también la capacidad de informar al compilador qué tipo de errores se pueden generar desde las funciones (y luego el compilador también podrá inferir cuáles son los tipos de errores que se pueden generar).

En mi opinión, las capturas mecanografiadas se pueden implementar sin las otras partes. Abra un nuevo problema, tal vez el equipo de TS decida marcarlo como duplicado, no lo sé.

@grofit

Lo que generaría el bit anterior, pero como azúcar sintáctico, incluso podría hacerlo como genérico, pero es un poco más desagradable:

try
{
  await doSomethingWhichReturnsPromise();
  send(200);
}
catch<NotFoundError>(error) { send(404); }
catch<SomeBadDataError>(error) { send(404); }
catch<CantSeeThisError>(error) { send(404); }
catch(error) { send(404); }

no va a funcionar porque implica la generación de código basado solo en tipos mientras que

 doSomethingWhichReturnsPromise()
    .then(send(200))
    .catch(NotFoundError, (error) => { send(404); })
    .catch(SomeBadDataError, (error) => { send(400); })
    .catch(CantSeeThisError, (error) => { send(403); })
    .catch((error) => { send(500); })

pasa la función Error en cada caso y es probable que se implemente con instanceof . Un código como ese es tóxico ya que fomenta la extensión Error lo cual es algo muy malo.

@nitzantomer Estoy de acuerdo en que es un tema aparte. El OP tiene ejemplos de tipos de captura que no se extienden Error .

@aluanhaddad
Para lo que pide @grofit , puede hacer algo como:

try {
    // do something
} catch (isNotFoundError(error)) {
    ...
}  catch (isSomeBadDataError(error)) {
    ...
} catch (error) {
    ...
}

Donde isXXX(error) son guardias tipo de la forma de:
function isXXX(error): error is XXX { ... }

@nitzantomer seguro, pero los protectores de tipo no son el problema. el problema es

class MyError extends Error {
  myErrorInfo: string;
}

lo cual es problemático, pero ya se ha discutido aquí. No deseo profundizar en el punto, pero muchas bases de código grandes y bien conocidas se han visto afectadas negativamente por la adopción de esta mala práctica. Extender Error es una mala idea.

@aluanhaddad
Soy consciente de eso, por lo que ofrecí una forma de lograr lo que solicitó @grofit pero sin la necesidad de extender Error . Un desarrollador aún podrá extender Error si lo desea, pero el compilador podrá generar código js que no necesita usar instanceof

@nitzantomer Me doy cuenta de que está al tanto, pero no me di cuenta de lo que le estaba sugiriendo a @grofit , lo que suena como una buena idea.

Solo estoy derrotando a un caballo muerto porque no me gusta tener que lidiar con API que quieren obligarme a usar tales patrones. De todos modos, lo siento si tomé esta discusión fuera del tema.

¿Se ha pensado más en esta discusión? Me encantaría ver una cláusula throws en mecanografiado.

@aluanhaddad Sigues diciendo que extender Error es una mala práctica. ¿Puedes elaborar un poco sobre esto?

Se ha discutido largamente. Básicamente, no puede extender de manera confiable los tipos integrados. El comportamiento varía drásticamente según la combinación de entornos y configuraciones de --target .

La única razón por la que pregunté es porque sus comentarios fueron los primeros que escuché de esto. Después de buscar un poco en Google, solo encontré artículos que explican cómo y por qué debería _debería_ extender Error .

No, no funcionará. Puede leer sobre esto en detalle en https://github.com/Microsoft/TypeScript/issues/12123 y los numerosos problemas vinculados.

Funcionará, pero solo en "entornos específicos", que se están convirtiendo en la mayoría a medida que se adapta ES6.

@nitzantomer : ¿Cree que es posible implementar una verificación de TSLint que informe al desarrollador cuando se llama a una función con @throws sin un try/catch circundante?

@bennyn Soy un usuario de TSLint, pero nunca he pensado en crear nuevas reglas.
Realmente no sé si es posible (aunque supongo que lo es), y si es así, qué tan fácil.

Si lo intenta, actualice aquí, gracias.

@shaipetel (y también @nitzantomer)

Re:

Sí, sé cómo funciona JavaScript, estamos hablando de TypeScript, que impone más limitaciones.
Sugerí, una buena solución en mi humilde opinión sería hacer que TypeScript siga el manejo de excepciones que requiere que todas las excepciones lanzadas sean de una clase base específica y no permitan arrojar valores sin envolver directamente.
Por lo tanto, no permitirá "arrojar 0" o "arrojar 'algún error'".
Al igual que JavaScript permite muchas cosas que TypeScript no permite.

No estoy seguro de si esto es lo que estabas sugiriendo @shaipetel, pero por si acaso... Te advierto que no hagas que Typescript restrinja throw para que solo devuelva Error s. ¡Las próximas funciones de renderizado asíncrono de React funcionan bajo el capó por throw ing Promise s! (Suena extraño, lo sé... pero he leído que han evaluado esto en comparación con los generadores async / await y yield / yield* para su caso de uso ¡y estoy seguro de que sé lo que están haciendo!)

throw ing Error s es, estoy seguro, el 99% de los casos de uso, pero no el 100%. No creo que Typescript deba restringir a los usuarios a solo throw cosas que amplían una clase base Error . Ciertamente es mucho más avanzado, pero throw tiene otros casos de uso además de errores/excepciones.

@mikeleonard
Estoy de acuerdo, hay muchos ejemplos de código js existente que arroja una variedad de tipos (he visto muchos throw "error message string" ).
La nueva función React es otro buen ejemplo.

Solo mis dos centavos, estoy convirtiendo el código a async/await y, por lo tanto, estoy sujeto a lanzamiento (aparte, odio las excepciones). En mi opinión, sería bueno tener una cláusula de lanzamiento como se discute en este problema. Creo que también sería útil incluso si se permitiera "lanzar cualquiera". (Además, tal vez una opción de "no lanzamientos" y compilador que predetermina las cosas a "no lanzamientos").

Parece una extensión natural para permitir escribir lo que arroja una función. Los valores de retorno se pueden escribir opcionalmente en TS y, para mí, parece que throw es solo otro tipo de retorno (como lo demuestran todas las alternativas sugeridas para evitar throw, como https://stackoverflow.com/a/39209039/162530).

(Personalmente, también me encantaría tener la opción de compilador (opcional) para hacer cumplir que cualquier persona que llame a una función declarada como lanzamientos también debe declararlos como lanzamientos o atraparlos).

Mi caso de uso actual: no quiero convertir toda mi base de código [Angular] para usar excepciones para el manejo de errores (ya que los odio). Uso async/await en los detalles de implementación de mis API, pero convierto throw en Promesas/Observables normales cuando regresa una API. Sería bueno que el compilador verifique que estoy captando las cosas correctas (o si no, idealmente).

@aleksey-bykov No cambiemos la naturaleza de JavaScript, solo agreguemos escritura. :)

agregar tipeo ya lo cambia (corta el código que no tiene sentido),
De la misma manera podemos reforzar el manejo de excepciones

El jueves, 26 de julio de 2018 a las 8:22 p. m., Joe Pea [email protected] escribió:

@aleksey-bykov https://github.com/aleksey-bykov No cambiemos el
naturaleza de JavaScript, vamos a agregarle tipeo. :)


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/Microsoft/TypeScript/issues/13219#issuecomment-408274156 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AA5PzfUNS5E093Z74WA4WCUaTyWRRZC3ks5uKl1FgaJpZM4LXwLC
.

Para promesas, si usa el encadenamiento then en lugar de async/await y evita usar throw , todo funciona muy bien en realidad. Aquí hay un boceto de prueba de concepto:

https://bit.ly/2NQZD8i - enlace del parque infantil

interface SafePromise<T, E> {
    then<U, E2>(
        f: (t: T) => SafePromise<U, E2>):
        SafePromise<U, E | E2>;

    catch<U, E2>(
        f: (e: E) => SafePromise<U, E2>):
        SafePromise<U, E2>

    catch<U, E1, E2>(
        guard: (e: any) => e is E1,
        f: (e: E) => SafePromise<U, E2>):
        SafePromise<U, Exclude<E, E1> | E2>
}

declare function resolve<T>(t:T): SafePromise<T, never>
declare function reject<E extends Error>(e:E): SafePromise<never, E>


class E404 extends Error {
    code:404 = 404;
}

class E403 extends Error {
    code:403 = 403;
    static check(e: any): e is E403 { return e && e.code === 403 }
}

let p = resolve(20)


let oneError = p.then(function f(x) {
    if (x > 5) return reject(new E403())
    return resolve(x)
})


let secondError = oneError.then(x => {
    if (x > 10) return reject(new E404())
    return resolve(x)
})

let remove403 = secondError.catch(E403.check, e => {
    return resolve(25)
})

let moreErrorsInfer = p.then(x => {
    if (x > 5) return reject(new E403())
    if (x > 10) return reject(new E404())
    return resolve(x)
})

let moreErrorsNoInfer = p.then((x):SafePromise<number, E403|E404> => {
    if (x > 5) return reject(new E403())
    if (x > 10) return reject(new E404())
    return resolve(x)
})

El predicado catch elimina tipos de la unión, mientras que devuelve reject() para agregarlos. Lo único que no funciona del todo es la inferencia de tipos de parámetros de unión (probablemente un error)

Problemas abordados en el ejemplo anterior:

  • no está restringido a errores, puede rechazar con cualquier cosa (aunque probablemente sea una mala idea)
  • no está restringido al uso de instanceof, puede usar cualquier protección de tipo de predicado para eliminar errores de la unión

El problema principal que veo al usar esto para todo el código son las firmas de devolución de llamada, para que funcione de manera compatible, el "tipo de devolución" predeterminado sería "might-throw-any", y si desea restringir eso, diría throws X o throws never si no es así, por ejemplo

declare function pureMap<T>(t:T[], f: (x:T) => U throws never):U[] throws never

La versión sin la firma:

declare function dirtyMap<T>(t:T[], f: (x:T) => U):U[]

en realidad sería por defecto

declare function dirtyMap<T>(t:T[], f: (x:T) => U throws any):U[] throws any

para garantizar que se compile todo el código actual.

A diferencia de strictNullChecks , donde los valores de retorno nulos son bastante poco comunes, creo que las excepciones en JS son bastante generalizadas. Modelarlos en archivos .d.ts puede no ser tan malo (importar tipos de dependencias para describir sus errores), pero definitivamente será un esfuerzo no trivial y dará como resultado uniones enormes.

Creo que un buen término medio sería centrarse en Promises y async/await , ya que Promise ya es un envoltorio, y el código asíncrono es donde el manejo de errores se bifurca más en escenarios típicos. Otros errores estarían "desmarcados"

@spion-h4 ¿esto (ya/lo hará) se puede usar en mecanografiado?

declare function dirtyMap<T>(t:T[], f: (x:T) => U throws any):U[] throws any

@bluelovers
no, TypeScript actualmente no admite la palabra clave throws , de eso se trata este problema.

Algo a tener en cuenta (no es que necesariamente bloquee esta propuesta) es que sin clases nominales, escribir errores incorporados aún no es particularmente útil ya que todos son estructuralmente iguales.

p.ej:

class AnotherError extends Error {}

function* range(min: number, max: number): Iterable<number> throws TypeError, RangeError {
  if (typeof min !== 'number') {
    throw new TypeError('min must be a number')
  }
  if (typeof min !== 'number') {
    throw new TypeError('max must be a number')
  }
  if (!Number.isSafeInteger(min)) {
    // Allowed because without nominal types we can't distinguish
    // Error/RangeError/TypeError/etc
    throw new Error('min must be a safe integer')
  }
  if (!Number.isSafeInteger(max)) {
    // Also allowed because AnotherError is also structurally
    // compatible with TypeError/RangeError
    throw new AnotherError('max must be a safe integer')
  }
  for (let i = min; i < max; i++) {
    yield i
  }
}

@jamesernator
Este tema no tiene nada que ver con la propuesta.
Puede meterse en el mismo problema exacto haciendo esto:

class BaseClass {
    propA!: string;
}

class MyClass1 extends BaseClass { }

class MyClass2 extends BaseClass { }

function fn(): MyClass1 {
    return new MyClass2();
}

Es una limitación del idioma que afecta a muchos casos de uso.

Hay una gran lectura de @ahejlsberg sobre excepciones escritas: https://www.artima.com/intv/handcuffs.html

Creo que TypeScript está en una buena posición para evitar estos problemas. TypeScript tiene que ver con soluciones _pragmáticas_ a problemas del _mundo_real_. En mi experiencia, en grandes bases de código de JavaScript y TypeScript, el manejo de errores es uno de los mayores problemas: ver qué funciones de errores _podrían_ arrojar y qué _podría_ querer manejar es increíblemente difícil. Solo es posible leyendo y escribiendo una buena documentación (todos sabemos lo buenos que somos en esto / s) o mirando la implementación, y dado que, a diferencia de los valores devueltos, las excepciones solo se propagan automáticamente a través de llamadas a funciones, no es suficiente solo verifique la función directamente llamada, pero para atraparlos a todos, también tendría que verificar las llamadas a funciones anidadas. _Este es un problema del mundo real_.

Los errores en JavaScript son en realidad valores muy útiles. Muchas API en NodeJS lanzan objetos de error detallados, que tienen códigos de error bien definidos y exponen metadatos útiles. Por ejemplo, los errores de child_process.execFile() tienen propiedades como exitCode y stderr , los errores de fs.readFile() tienen códigos de error como ENOENT (archivo no encontrado ) o EPERM (permisos insuficientes). Conozco muchas bibliotecas que también hacen esto, por ejemplo, el controlador de base de datos pg le brinda suficientes metadatos en un error para saber qué restricción de columna exacta causó que fallara un INSERT .

Puede ver una cantidad preocupante de verificaciones de expresiones regulares frágiles en los mensajes de error en las bases de código porque las personas no son conscientes de que los errores tienen códigos de error adecuados y cuáles son.

Si pudiéramos definir en declaraciones @types y lib.d.ts qué errores pueden arrojar estas funciones, TypeScript tendría el poder de ayudarnos con la estructura del error: qué posibles errores puede haber, cuál es el los valores del código de error son, qué propiedades tienen. Esto _no_ se trata de excepciones escritas y, por lo tanto, evita por completo todos los problemas con las excepciones escritas. Es una _solución pragmática_ a un _problema del mundo real_ (al contrario de decirle a la gente que use valores de retorno de error monádicos; la realidad de JavaScript parece diferente, las funciones arrojan errores).

La anotación puede ser completamente opcional (o requerida con un indicador del compilador). Si no se especifica en un archivo de declaración, una función simplemente arroja any (o unknown ).
Una función puede declarar manualmente nunca lanzar con throws never .
Si la implementación está disponible, una función arroja una unión de todos los tipos de excepción de las funciones a las que llama y sus propias instrucciones throw que no están dentro de un bloque try con un catch cláusula
Si uno de ellos arroja any , la función arroja any también; está bien, en cada límite de función, el desarrollador tiene la oportunidad de corregirlo a través de una anotación explícita.
Y en muchos casos, cuando se llama a una sola función conocida y se envuelve en un intento/captura (por ejemplo, leer un archivo y manejarlo sin que se encuentre), TypeScript puede inferir el tipo en la cláusula de captura.

No necesitamos la subclasificación de errores para esto ni instanceof : los tipos de error pueden ser interfaces, que especifican códigos de error con cadenas literales, y TypeScript puede discriminar la unión en el código de error o usar protectores de tipo.

Definición de tipos de error

interface ExecError extends Error {
  status: number
  stderr: Buffer
}
function execFileSync(cmd: string): Buffer throws ExecError;
interface NoEntityError extends Error { code: 'ENOENT' }
interface PermissionError extends Error { code: 'EPERM' }
function readFileSync(file: string): Buffer throws NoEntityError | PermissionError;



md5-818797fe8809b5d8696f479ce1db4511



Preventing a runtime error due to type mismatch



md5-c2d214f4f8ecd267a9c9252f452d6588



Catching errors with type switch



md5-75d750bbe0c3494376581eaa3fa62ce5



```ts
try {
  const resp = await fetch(url, { signal })
  if (!resp.ok) {
    // inferred error type
    // look ma, no Error subclassing!
    throw Object.assign(new Error(resp.statusText), { name: 'ResponseError', response })
  }
  const data = await resp.json()
} catch (err) { // AbortError | Error & { name: 'ResponseError', response: Response } | SyntaxError
  switch (err.name)
    case 'AbortError': return; // Don't show AbortErrors
    default: displayError(err); return;
  }
}



md5-a859955ab2c42d8ce6aeedfbb6443e93



```ts
interface HttpError extends Error { status: number }
// Type-safe alternative to express-style middleware request patching - just call it (TM)
// The default express error handler recognises the status property
function checkAuth(req: Request): User throws HttpError {
    const header = req.headers.get('Authorization')
    if (!header) {
        throw Object.assign(new Error('No Authorization header'), { status: 401 })
    }
    try {
        return parseHeader(header)
    } catch (err) {
        throw Object.assign(new Error('Invalid Authorization header'), { status: 401 })
    }
}

Aparte de los errores, ¿también es posible marcar otros tipos de efectos secundarios como

  • Divergencia (bucle infinito / sin retorno)
  • IO
  • No determinismo ( Math.random )

Me recuerda a Koka de MSR, que puede etiquetar efectos en los tipos que regresan.

La propuesta:

function square(x: number): number &! never { // This function is pure
  return x * x
}

function square2(x : number) : number &! IO {
  console.log("a not so secret side-effect")
  return x * x
}

function square3( x : number ) : number &! Divergence {
  square3(x)
  return x * x
}

function square4( x : number ) : number &! Throws<string> { // Or maybe a simple `number &! Throws`?
  throw "oops"
  return x * x
}

function map<T, R, E>(a: T[], f :(item: T) => R &! E): R[] &! E { ... }
function map<T, R>(a: T[], f :(item: T) => R): R[] { ... } // will also work; TS would collect side effects

function str<T>(x: T): string &! (T.toString.!) // care about the side effect of a type

¡Me encanta la idea de la declaración del tipo de error! Esto sería de gran ayuda para las personas que lo necesitan y no hará nada malo para las personas a las que no les gusta. Ahora tengo el problema en mi proyecto de nodo, que podría resolverse más rápido con esta función. Tengo que detectar errores y devolver el código http adecuado; para hacerlo, siempre tengo que verificar todas las llamadas a funciones para descubrir las excepciones que tengo que manejar, y esto no es divertido -_-

PD. La sintaxis en https://github.com/Microsoft/TypeScript/issues/13219#issuecomment -428696412 se ve muy bien: &! y los tipos de errores de ajuste en <> simplemente increíbles.

Yo también estoy a favor de esto. Ahora todos los errores no están tipificados y eso es bastante molesto. Aunque espero que la mayoría de los throws se puedan derivar del código para que tengamos que escribirlo en todas partes.

De esta manera, las personas pueden incluso escribir reglas tslint que obligan al desarrollador a detectar errores hostiles para el usuario en puntos finales de descanso, etc.

De hecho, me sorprendió que esta funcionalidad no estuviera _ya_ dentro de TypeScript. Una de las primeras cosas que fui a declarar. Estaría bien si el compilador lo hace cumplir o no, siempre que pueda obtener la información de que se producirá un error y qué tipo de errores se producirán.

Incluso si no obtenemos el &! y solo obtenemos el Throws<T> sería :+1:

Esto es exactamente lo que tengo en mente, sería mucho más hermoso si TypeScript admitiera estas cláusulas.

Otra característica posible sería forzar de alguna manera (quizás a nivel de función o a nivel de módulo) el seguimiento de excepciones estrictas, lo que significaría que tendría que invocar cualquier cosa declarada como throws X con una expresión try! invokeSomething() como en Rust y Swift.

Simplemente se compilará en invokeSomething() pero la posibilidad de excepciones será visible en el código, lo que facilita la detección si está dejando algo mutable en un mal estado transitorio (o dejando recursos asignados sin desechar)

@be5invis

function square(x: number): number &! never { // This function is pure
  return x * x
}
...

Solo estoy dando mi granito de arena aquí, pero el &! me parece increíblemente feo.
Creo que las personas que son nuevas en el mecanografiado se sentirían un poco desconcertadas por este símbolo, es realmente poco atractivo.
Un simple throws es más explícito, intuitivo y simple, en mi opinión.

Aparte de eso, +1 para excepciones escritas. 👍

Me gustaría agregar mi +1 para las excepciones escritas y verificadas.
He hecho un gran uso de las excepciones escritas en mi código. Soy plenamente consciente de las trampas de instanceof , de hecho lo he usado para mi ventaja al poder escribir controladores genéricos para errores relacionados que se heredan de una clase base común. La mayoría de los otros métodos que he encontrado para evitar la herencia de la clase Error base terminan siendo (al menos) igualmente complejos y problemáticos de diferentes maneras.
Las excepciones marcadas son una mejora que, en mi opinión, se presta a una mayor comprensión del análisis estático del código base. En una base de código de suficiente complejidad, puede ser fácil pasar por alto qué excepciones lanza una función en particular.

Para aquellos que buscan seguridad de errores en tiempo de compilación en mecanografiado, pueden usar mi biblioteca ts-results .

@vultix para mí, lamentablemente, la separación no es lo suficientemente clara como para poner esto en un entorno de equipo.

@vultix El enfoque de su lib se ha discutido anteriormente, y es exactamente lo contrario de lo que esta característica está aquí para resolver.
Javascript ya tiene el mecanismo para manejar errores, se llama excepciones, pero el mecanografiado carece de una forma de describirlo.

@nitzantomer Estoy completamente de acuerdo en que esta característica es una necesidad para el mecanografiado. Mi biblioteca no es más que un sustituto temporal hasta que se agregue esta característica.

function* range(min: number, max: number): Iterable<number> throws TypeError, RangeError {

Sé que llegué un poco tarde a este juego en esto, pero el siguiente parece un poco más de sintaxis Typescripty en lugar de coma.

Iterable<number> throws TypeError | RangeError

Pero todavía soy bueno con la coma... Ojalá tuviéramos esto en el idioma.
La principal es que me gustaría en JSON.parse() porque muchos de mis compañeros de trabajo parecen olvidar que JSON.parse puede arrojar un error y ahorraría mucho de ida y vuelta con solicitudes de extracción.

@gusanos
Estoy completamente de acuerdo.
Mi propuesta incluía la sintaxis que recomiendas:

function mightThrow(): void throws string | MyErrorType { ... }

¿Podemos seguir el patrón de declaración de errores de un lenguaje OOP como Java? La palabra clave "throws" se usa para declarar que necesitamos "intentar ... atrapar" cuando usamos una función con un error potencial

@allicanseenow
La propuesta es para el uso opcional de la cláusula throws.
Es decir, no tendrá que usar try/catch si está usando una función que lanza.

@allicanseenow , es posible que desee leer mi artículo anterior para obtener contexto, incluido el artículo vinculado: https://github.com/microsoft/TypeScript/issues/13219#issuecomment -416001890

Creo que esta es una de las cosas que faltan en el sistema de tipos mecanografiados.
Es puramente opcional, no cambia el código emitido, ayuda a trabajar con bibliotecas y funciones nativas.

Además, saber qué errores se pueden generar podría permitir a los editores "autocompletar" la cláusula catch con condiciones if para manejar todos los errores.

Quiero decir, realmente incluso una aplicación pequeña merece un manejo de errores adecuado; lo peor ahora es que los usuarios no saben cuándo algo puede salir mal.

Para mí, esta es la principal característica que falta ahora.

@RyanCavanaugh , ha habido muchos comentarios desde que se agregó la etiqueta "Esperando más comentarios". ¿Algún miembro del equipo mecanografiado puede opinar?

@nitzantomer Apoyo la idea de agregar lanzamientos a TypeScript, pero ¿no permitiría una prueba/captura opcional declaraciones de lanzamientos potencialmente inexactas cuando se usan en funciones anidadas?

Para que un desarrollador confíe en que la cláusula throws de un método es precisa, tendría que asumir la peligrosa suposición de que en ningún punto de la jerarquía de llamadas de ese método se ignoró una excepción opcional y se arrojó a la pila sin informar. Creo que @ajxs puede haberlo mencionado al final de su comentario, pero creo que esto sería un gran problema. Especialmente con lo fragmentadas que están la mayoría de las bibliotecas npm.

@ConnorSinnott
Espero haberte entendido bien:

El compilador inferirá tipos arrojados, por ejemplo:

function fn() {
    if (something) {
        throw "something happened";
    }
}

En realidad será function fn(): throws string { ... } .
El compilador también generará un error cuando haya una discrepancia entre los errores declarados y los reales:

function fn1() throws string | MyError {
    ...
}

function fn2() throws string {
    fn1(); // throws but not catched

    if (something) {
        throws "damn!";
    }
}

El compilador debería quejarse de que fn2 arroja string | MyError y no string .

Con todo esto en mente, no veo cómo esta suposición es más peligrosa que la suposición de que otros tipos declarados en los que un desarrollador confía cuando usa otras bibliotecas, marcos, etc.
No estoy seguro de haber cubierto realmente todas las opciones, y me alegraría si pudieras pensar en un escenario interesante.

E incluso con este problema, es más o menos lo mismo que ahora de todos modos:

// can throw SyntaxError
declare function a_fancy_3rd_party_lib_function(): MyType;

function fn1() {
    if (something) {
        throws "damn!";
    }

    if (something else) {
        throws new Error("oops");
    }

    a_fancy_3rd_party_lib_function();
}

function fn2() {
    try {
        fn1();
    } catch(e) {
        if (typeof e === "string") { ... }
        else if (e instanceof MyError) { ... }
    }
}

Podemos hacer que mecanografiado nos proteja incluso de esto, pero requerirá una sintaxis que es un poco diferente de javascript:

function fn2() {
    try {
        fn1();
    } catch(typeof e === "string") {
        ...
    } catch(e instanceof MyError) {
        ...
    }
}

Esto se compilará para:

function fn2() {
    try {
        fn1();
    } catch(e) {
        if (typeof e === "string") {}
        else if (e instanceof MyError) {} 
        else {
            throw e;
        }
    }
}

¡Oye! ¡Gracias por responder! En realidad, tener los lanzamientos inferidos como mencionaste mitiga este problema en el que estaba pensando. Pero el segundo ejemplo que enumeraste es interesante.

Dado

function first() : string throws MyVeryImportantError { // Compiler complains: missing number and string
    if(something) {
        throw MyVeryImportantError
    } else {
        return second().toString();
    }
}

function second() : number {
    if(something)
        throw 5;
    else   
        return third();
}

function third() : number throws string {
    if(!something)
        throw 'oh no!';
    return 9;
}

Para declarar explícitamente MyVeryImportantError, también tendría que declarar explícitamente todos los errores adicionales de la pila de llamadas, que podrían ser un puñado dependiendo de la profundidad de la aplicación. Además, potencialmente tedioso. Ciertamente no me gustaría sumergirme en toda la cadena de llamadas para generar una lista de posibles errores que podrían surgir en el camino, pero tal vez el IDE podría ayudar.

Estaba pensando en proponer algún tipo de operador de propagación para permitir que el desarrollador declare explícitamente su error y simplemente arroje el resto.

function first() : string throws MyVeryImportantError | ... { // Spread the rest of the errors

Pero habría una manera más fácil de lograr el mismo resultado: simplemente suelte la declaración throws.

function first() : string { // Everything automatically inferred

Lo que plantea la pregunta: ¿cuándo vería un beneficio al usar la palabra clave throws en lugar de simplemente dejar que TypeScript infiera los errores?

@ConnorSinnott

No es diferente de otros lenguajes que usan la cláusula throw, es decir, java.
Pero creo que en la mayoría de los casos no tendrás que lidiar con muchos tipos. En el mundo real, normalmente solo tratará con string , Error (y subclases) y diferentes objetos ( {} ).
Y en la mayoría de los casos, solo podrá usar una clase principal que capture más de un tipo:

type MyBaseError = {
    message: string;
};

type CodedError = MyBaseError & {
    code: number;
}

function fn1() throws MyBaseError {
    if (something) {
        throw { message: "something went wrong" };
    }
}

function fn2() throw MyBaseError {
    if (something) {
        throw { code: 2, message: "something went wrong" };
    }

    fn1();
}

En cuanto al uso implícito de la cláusula throw en lugar de que el compilador la infiera, creo que es como declarar tipos en mecanografiado, en muchos casos no es necesario, pero puede hacerlo para documentar mejor su código para que quien lo lea después podrá entenderlo mejor.

Otro enfoque si desea seguridad de tipo es simplemente tratar los errores como valores a la Golang:
https://gist.github.com/brandonkal/06c4a9c630369979c6038fa363ec6c83
Aún así, esta sería una buena característica para tener.

@brandonkal
Ese enfoque ya se discutió anteriormente en el hilo.
Es posible, pero hacerlo es ignorar una parte/característica inherente que nos permite javascript.

También creo que el especificador noexcept (#36075) es la mejor y más simple solución en este momento, ya que para la mayoría de los programadores, lanzar excepciones considera un antipatrón.

Aquí hay algunas características interesantes:

  1. La verificación de funciones no arrojará un error:
noexcept function foo(): void {
  throw new Error('Some exception'); // <- compile error!
}
  1. Advertencia sobre bloques redundantes try..catch :
try { // <- warning!
  foo();
} catch (e) {}
  1. Resuelve siempre las promesas:
interface ResolvedPromise<T> extends Promise<T> {
  // catch listener will never be called
  catch(onrejected?: noexcept (reason: never) => never): ResolvedPromise<T>; 

  // same apply for `.then` rejected listener,
  // resolve listener should be with `noexpect`
}

@moshest

como para la mayoría de los programadores, lanzar excepciones se considera un antipatrón.

Supongo que no soy parte de ese grupo de "la mayoría de los programadores".
El noexcept es genial y todo, pero no es de lo que se trata este problema.

Si las herramientas están ahí, las usaré. throw , try/catch y reject están ahí, así que los usaré.

Sería bueno escribirlos correctamente en mecanografiado.

Si las herramientas están ahí, las usaré. throw , try/catch y reject están ahí, así que los usaré.

Seguro. Los usaré también. Mi punto es que noexcept o throws never serán un buen comienzo para este problema.

Ejemplo tonto: quiero saber que si llamaré a Math.sqrt(-2) nunca arrojará ningún error.

Lo mismo se aplica a las bibliotecas de terceros. Da más control sobre el código y los casos extremos que necesito manejar como programador.

@moshest
¿Pero por qué?
Esta propuesta incluye todos los beneficios de la propuesta 'noexpect', así que ¿por qué conformarse con menos?

Porque toma años (este problema tiene 3 años), y esperaba que al menos podamos comenzar primero con la función más básica.

@moshest
Bueno, prefiero una característica mejor que tome más tiempo que una solución parcial que tomará menos tiempo pero nos quedaremos con ella para siempre.

Siento que noexcept se puede agregar fácilmente más adelante, de hecho, podría ser un tema separado.

noexcept es lo mismo que throws never .

TBH, creo que es más importante tener algún mecanismo que garantice el manejo de excepciones (ala no se usa error en Go) en lugar de proporcionar sugerencias de tipo para try/catch

@roll no debería haber ninguna "garantía".
no es obligatorio manejar excepciones en javascript y tampoco debería serlo en mecanografiado.

Podría ser una opción como parte del modo estricto de typescrypt que una función debe detectar el error o declararlo explícitamente throws y pasarlo

Para agregar mis 5 centavos: veo excepciones similares a una función que devuelve null : es algo que normalmente no espera que suceda. Pero si sucede, se produce un error de tiempo de ejecución, lo cual es malo. Ahora, TS agregó "tipos que no aceptan valores NULL" para recordarle cómo manejar null . Veo que agregar throws es llevar estos esfuerzos un paso más allá, al recordarle que también debe manejar las excepciones. Es por eso que creo que esta característica es definitivamente necesaria.

Si observa Go, que devuelve errores en lugar de arrojarlos, puede ver aún más claro que ambos conceptos no son tan diferentes. Además, te ayuda a entender algunas API más profundamente. Tal vez no sepa que algunas funciones pueden lanzar y lo nota mucho más tarde en la producción (por ejemplo JSON.parse , ¿quién sabe cuántas más hay?).

@ obedm503 Esta es en realidad una filosofía de TS: por defecto, el compilador no se queja de nada. Puede habilitar opciones para tratar ciertas cosas como un error o habilitar el modo estricto para habilitar todas las opciones a la vez. Entonces, esto debería ser un hecho.

Me encanta esta característica sugerida.
La tipificación e inferencia de excepciones puede ser una de las mejores cosas en toda la historia de la programación.
❤️

Hola, acabo de pasar un poco de tiempo tratando de encontrar una solución a lo que tenemos actualmente en TS. Como no hay forma de obtener el tipo de los errores arrojados dentro del alcance de una función (ni obtener el tipo del método actual, por cierto), descubrí cómo podríamos, sin embargo, establecer explícitamente los errores esperados dentro del método en sí. Más tarde, podríamos recuperar esos tipos y al menos saber qué se podría arrojar dentro del método.

Aquí está mi POC

/***********************************************
 ** The part to hide a type within another type
 **********************************************/
// A symbol to hide the type without colliding with another existing type
const extraType = Symbol("A property only there to store types");

type extraType<T> = {
    [extraType]?: T;
}

// Set an extra type to any other type
type extraTyped<T, E> = T & extraType<E>

// Get back this extra type
type getExtraType<T> = T extends extraType<infer T> ? T : never;

/***********************************************
 ** The part to implement a throwable logic
 **********************************************/

// Throwable is only a type holding the possible errors which can be thrown
type throwable<T, E extends Error> = extraTyped<T,E>

// return the error typed according to the throwableMethod passed into parameter
type basicFunction = (...any: any[]) => any;
const getTypedError = function<T extends basicFunction> (error, throwableMethod:T) {
    return error as getExtraType<ReturnType<T>>;
};

/***********************************************
 ** An example of usage
 **********************************************/

class CustomError extends Error {

}

// Here is my unreliable method which can crash throwing Error or CustomError.
// The returned type is simply our custom type with what we expect as the first argument and the
// possible thrown errors types as the second (in our case a type union of Error and CustomError)
function unreliableNumberGenerator(): throwable<number, Error | CustomError> {

    if (Math.random() > 0.5) {
        return 42;
    }

    if (Math.random() > 0.5) {
        new Error('No luck');
    }

    throw new CustomError('Really no luck')
}

// Usage
try {
    let myNumber = unreliableNumberGenerator();
    myNumber + 23;
}

// We cannot type error (see TS1196)
catch (error) {
    // Therefore we redeclare a typed value here and we must tell the method which could have crashed
    const typedError = getTypedError(error, unreliableNumberGenerator);

    // 2 possible usages:
    // Using if - else clauses
    if (typedError instanceof CustomError) {

    }

    if (typedError instanceof Error) {

    }

    // Or using a switch case on the constructor:
    // Note: it would have been really cool if TS did understood the typedError.constructor is narrowed by the types Error | CustomError
    switch (typedError.constructor) {
        case Error: ;
        case CustomError: ;
    }

}

// For now it is half a solution as the switch case is not narrowing anything. This would have been 
// possible if the typedError would have been a string union however it would not be reliable to rely
// on typedError.constructor.name (considering I would have find a way to convert the type union to a string union)

¡Muchas gracias por sus comentarios positivos! Me doy cuenta de que podría ser más fácil refactorizar el código existente sin tener que envolver todo el tipo devuelto en el tipo throwable . En su lugar, podríamos simplemente agregar ese al tipo devuelto, el siguiente código le permite agregar los errores arrojables de la siguiente manera:

// now only append '& throwable<ErrorsThrown>' to the returned type
function unreliableNumberGenerator(): number & throwable<Error | CustomError> { /* code */ }

Este es el único cambio para la parte de ejemplo, aquí está la declaración de nuevos tipos:

/***********************************************
 ** The part to hide a type within another type
 **********************************************/
// A symbol to hide the type without colliding with another existing type
const extraType = Symbol("A property only there to store types");

type extraType<T> = {
    [extraType]?: T;
}

// Get back this extra type
type getExtraType<T> = T extends extraType<infer T> ? T : never;

/***********************************************
 ** The part to implement a throwable logic
 **********************************************/

// Throwable is only a type holding the possible errors which can be thrown
type throwable<E extends Error> = extraType<E>

// return the error typed according to the throwableMethod passed into parameter
type basicFunction = (...any: any[]) => any;

type exceptionsOf<T extends basicFunction> = getExtraType<ReturnType<T>>;

const getTypedError = function<T extends basicFunction> (error: unknown, throwableMethod:T) {
    return error as exceptionsOf<T>;
};

También agregué un nuevo tipo exceptionsOf que permite extraer los errores de una función para escalar la responsabilidad. Por ejemplo:

function anotherUnreliableNumberGenerator(): number & throwable<exceptionsOf<typeof unreliableNumberGenerator>> {
// I don't want to use a try and catch block here
    return (Math.random() > 0.5) ? unreliableNumberGenerator() : 100;
}

Como exceptionsOf obtiene una combinación de errores, puede escalar tantos métodos críticos como desee:

function aSuperUnreliableNumberGenerator(): number & throwable<exceptionsOf<typeof unreliableNumberGenerator> | exceptionsOf<typeof anotherUnreliableNumberGenerator>> {
// I don't want to use a try and catch block here
    return (Math.random() > 0.5) ? unreliableNumberGenerator() : unreliableNumberGenerator();
}

No me gusta el uso de typeof , si encuentro una mejor manera te lo haré saber

Puede probar aquí los consejos de resultados : desplace typedError en la línea 106

@Xample ¡ Esa es una gran solución con las herramientas que tenemos ahora!
Pero creo que en la práctica no es suficiente ya que todavía puedes hacer cosas como:

const a = superRiskyMethod();
const b = a + 1;

Y el tipo de b se infiere como un número que es correcto pero solo si está envuelto dentro de un intento
Esto no debería ser válido.

@luisgurmendezMLabs No entiendo muy bien lo que quieres decir. Si sigue el repositorio de @Xample en la línea 56, puede ver que el resultado de myNumber + 23 se infiere como number , mientras que myNumber: number & extraType<Error | CustomError> .

En otra fase, a en su ejemplo nunca está envuelto en algo como Try Monad. No tiene ningún contenedor más que una intersección con & extraType<Error | CustomError> .

Felicitaciones por el increíble diseño @Xample 👏👏👏👏👏👏. Esto es realmente prometedor y ya es útil incluso sin azúcar sintáctico. ¿Tiene algún plan para construir una biblioteca de tipos para esto?

@ivawzh Mis pensamientos son que en la práctica esto podría traer algunos problemas ya que:

function stillARiskyMethod() { 
    const a = superRiskyMethod();
    return a + 1
}

Ese retorno de tipo de función se infiere como número y eso no es del todo correcto

@luisgurmendezMLabs el tipo arrojable está dentro del tipo de retorno de la función solo como una forma de pegar el tipo de error con la función (y poder recuperar esos tipos más adelante). Nunca devuelvo errores de verdad, solo los tiro. Por lo tanto, si está dentro de un bloque de prueba o no, no cambiará nada.

@ivawzh gracias por preguntar, lo acabo de hacer por ti

@luisgurmendezMLabs ah ok entiendo tu punto, parece mecanografiado solo inferir el primer tipo encontrado. Por ejemplo, si tuvieras:

function stillARiskyMethod() { 
    return superRiskyMethod();
}

el tipo de retorno de stillARiskyMethod se inferiría correctamente, mientras que

function stillARiskyMethod() { 
    return Math.random() < 0.5 superRiskyMethod() : anotherSuperRiskyMethod();
}

solo escribiría el tipo de retorno como superRiskyMethod() y descartaría el de anotherSuperRiskyMethod() . no he investigado mas

Por este motivo, debe escalar manualmente el tipo de error.

function stillARiskyMethod(): number & throwable<exceptionOf<typeof superRiskyMethod>> { 
    const a = superRiskyMethod();
    return a + 1
}

También quería dejar caer mis pensamientos sobre esto.

Creo que esta sería una característica realmente buena para implementar, ya que hay diferentes casos de uso para querer devolver un Error/null y querer arrojar algo y podría tener mucho potencial. Lanzar excepciones es parte del lenguaje Javascript de todos modos, entonces, ¿por qué no deberíamos dar la opción de escribir e inferir esto?

Por ejemplo, si estoy haciendo muchas tareas que pueden generar errores, me resultaría inconveniente tener que usar una declaración if para verificar el tipo de retorno cada vez, cuando esto podría simplificarse usando un intento/captura donde si hay alguno una de estas tareas lanza, se manejará en la captura sin ningún código adicional.

Esto es particularmente útil cuando va a manejar los errores de la misma manera ; por ejemplo, en express/node.js, es posible que desee pasar el error a NextFunction (controlador de errores).

En lugar de hacer if (result instanceof Error) { next(result); } cada vez, podría envolver todo el código para estas tareas en un intento/captura, y en mi captura que, dado que se lanzó una excepción, siempre querré pasar esto a mi controlador de errores, también puede catch(error) { next(error); }

Tampoco he visto esto discutido todavía (¡Es posible que lo haya perdido, sin embargo, este hilo tiene bastantes comentarios!) cláusula throws en su declaración de función? Siento que sería bueno hacer esto (no estamos obligando a las personas a manejar los lanzamientos, solo les informaría que la función arroja) pero la gran preocupación aquí es que si Typescript se actualizó de esta manera, probablemente romper un montón de código existente actualmente.

Editar: otro caso de uso que pensé podría ser que esto también ayudaría con la generación de JSDocs usando la etiqueta @throws

Repetiré aquí lo que dije en el tema ahora comprometido.


Creo que TypeScript debería poder inferir el tipo de error de la mayoría de las expresiones de JavaScript. Permitiendo una implementación más rápida por parte de los creadores de bibliotecas.

function a() {
  if (Math.random() > .5) {
    throw 'unlucky';
  }
}

function b() {
  a();
}

function c() {
  if (Math.random() > .5) {
    throw 'unlucky';
  }
  if (Math.random() > .5) {
    throw 'fairly lucky';
  }
}

function d() {
  return eval('You have no IDEA what I am capable of!');
}

function e() {
  try {
    return c;
  } catch(e) {
    throw 'too bad...';
  }
}

function f() {
  c();
}

function g() {
  a();
  c();
}
  • Función a sabemos que el tipo de error es 'unlucky' , y si queremos ser muy cautelosos podemos extenderlo a Error | 'unlucky' , por lo tanto includeError .
  • La función b heredará el tipo de error de la función a .
  • La función c es casi idéntica; 'unlucky' | 'fairly lucky' , o Error | 'unlucky' | 'fairly lucky' .
  • La función d tendrá que arrojar unknown , ya que eval es... desconocido
  • La función e detecta el error de d , pero dado que hay throw en el bloque catch, inferimos su tipo 'too bad...' , aquí, ya que el bloque solo contiene throw 'primitive value' , podríamos decir que no puede arrojar Error (Corrígeme si me perdí algo de magia negra de JS...)
  • La función f hereda de c igual que b de a .
  • La función g hereda 'unlucky' de a y unknown de c así 'unlucky' | unknown => unknown

Aquí hay algunas opciones del compilador que creo que deberían incluirse, ya que el compromiso de los usuarios con esta función puede variar según su habilidad, así como la seguridad de tipo de las bibliotecas de las que dependen en un proyecto determinado:

{
  "compilerOptions": {
    "errorHandelig": {
      "enable": true,
      "forceCatching": false,   // Maybe better suited for TSLint/ESLint...
      "includeError": true, // If true, every function will by default throw `Error | (types of throw expressions)`
      "catchUnknownOnly": false, // If true, every function will by default throw `unknown`, for those very skeptics (If someone replaces some global with weird proxy, for example...)
      "errorAsesertion": true,  // If false, the user will not be able to change error type manually
    }
  }
}

En cuanto a la sintaxis sobre cómo expresar el error que puede producir cualquier función, no estoy seguro, pero sé que necesitamos la capacidad para que sea genérico e inferible.

declare function getValue<T extends {}, K extends keyof T>(obj: T, key: K): T[K] throws void;
declare function readFile<throws E = 'not valid file'>(file: string, customError: E): string throws E;

Mi caso de uso, ya que mostrar un caso de uso real podría mostrar otro que tiene un valor:

declare function query<T extends {}, throws E>(sql: string, error: E): T[] throws E;

app.get('path',(req, res) => {
  let user: User;
  try {
    user = query('SELECT * FROM ...', 'get user');
  } catch(e) {
    return res.status(401);
  }

  try {
    const [posts, followers] = Promise.all([
      query('SELECT * FROM ...', "user's posts"),
      query('SELECT * FROM ...', "user's follower"'),
    ]);

    res.send({ posts, followers });
  } catch(e) {
    switch (e) {
      case "user's posts":
        return res.status(500).send('Loading of user posts failed');

      case "user's posts":
        return res.status(500).send('Loading of user stalkers failed, thankfully...');

      default:
        return res.status(500).send('Very strange error!');
    }
  }
});

Necesito un sumidero de errores para evitar enviar encabezados de respuesta varias veces para múltiples errores (por lo general, no suceden, pero cuando lo hacen, ¡lo hacen de forma masiva!)

Este problema todavía está etiquetado Awaiting More Feedback , ¿qué podemos hacer para proporcionar más comentarios? Esta es la única característica que envidio el lenguaje java.

Tenemos un caso de uso en el que arrojamos un error específico cuando una llamada a la API devuelve un código que no es 200:

interface HttpError extends Error {
  response: Response
}

try {
  loadData()
} catch (error: Error | ResponseError) {
  if (error.response) {
    checkStatusCodeAndDoSomethingElse()
  } else {
    doGenericErrorHandling()
  }
}

No poder escribir el bloque catch hace que los desarrolladores olviden que se pueden generar 2 posibles tipos de errores y que deben manejar ambos.

Prefiero lanzar siempre el objeto Error:

function fn(num: number): void {
    if (num === 0) {
        throw new TypeError("Can't deal with 0")
    }
}
try {
 fn(0)
}
catch (err) {
  if (err instanceof TypeError) {
   if (err.message.includes('with 0')) { .....}
  }
}

¿Por qué esta característica todavía "no tiene suficientes comentarios"? Es muy útil cuando se invoca la API del navegador como indexedDB, localstorage. Ha causado muchas fallas en escenarios complejos, pero el desarrollador no puede darse cuenta en la programación.

Hegel parece tener perfectamente esta característica.
https://hegel.js.org/docs#benefits (desplácese hasta la sección "Error escrito")

¡Ojalá TypeScript tenga una característica similar!

DL;RD;

  • La función de rechazo de las promesas debe escribirse
  • Cualquier tipo arrojado dentro de un bloque try debe inferirse usando una unión en el argumento de error de catch
  • error.constructor debe escribirse correctamente usando el tipo real y no solo any para evitar perder un error arrojado.

Bien, tal vez deberíamos simplemente aclarar cuáles son nuestras necesidades y expectativas:

Los errores generalmente se manejan de 3 maneras en js

1. El camino del nodo

Uso de devoluciones de llamada (que en realidad se pueden escribir)

Ejemplo de uso:

import * as fs from 'fs';

fs.readFile('readme.txt', 'utf8',(error, data) => {
    if (error){
        console.error(error);
    }
    if (data){
        console.log(data)
    }
});

Donde fs.d.ts nos da:

function readFile(path: PathLike | number, options: { encoding: string; flag?: string; } | string, callback: (err: NodeJS.ErrnoException | null, data: string) => void): void;

Por lo tanto, el error se escribe así

    interface ErrnoException extends Error {
        errno?: number;
        code?: string;
        path?: string;
        syscall?: string;
        stack?: string;
    }

2. El camino de la promesa

Cuando la promesa se resuelva o se rechace, aunque puede escribir el value resuelto, no puede escribir el rechazo, a menudo llamado reason .

Aquí está la firma del constructor de una promesa: Tenga en cuenta que el motivo se escribe any

    new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;

En teoría, podría haber sido posible escribirlos de la siguiente manera:

    new <T, U=any>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: U) => void) => void): Promise<T>;

De esta manera, teóricamente aún podríamos hacer una conversión 1-1 entre una devolución de llamada de nodo y una promesa, manteniendo todo el tipeo a lo largo de este proceso. Por ejemplo:

const fs = require('fs')
const util = require('util')

const readFilePromise = util.promisify(fs.readFile); // (path: PathLike | number, options: { encoding: string; flag?: string; } | string) => Promise<data: string, NodeJS.ErrnoException>;

3. La forma de "intentar y atrapar"

try {
    throw new Error();
}
catch (error: unknown) { //typing the error is possible since ts 4.0
    if (error instanceof Error) {
        error.message;
    }
}

A pesar de que arrojamos un error "Error" 2 líneas antes del bloque catch, TS no puede escribir error (valor) como Error (tipo).

¿No es el caso ni el uso de promesas dentro de una función async (no hay magia "todavía"). Usando nuestra devolución de llamada de nodo prometida:

async function Example() {
    try {
        const data: string = await readFilePromise('readme.txt', 'utf-8');
        console.log(data)
    }
    catch (error) { // it can only be NodeJS.ErrnoException, we can type is ourself… but nothing prevents us to make a mistake
        console.error(error.message);
    }
}

No falta información para que TypeScript pueda predecir qué tipo de error podría generarse dentro de un ámbito de prueba.
Sin embargo, debemos considerar que las funciones nativas internas pueden generar errores que no están dentro del código fuente; sin embargo, si cada vez que una palabra clave "lanzar" está en la fuente, TS debe recopilar el tipo y sugerirlo como un tipo posible para el error. Esos tipos, por supuesto, estarían dentro del alcance del bloque try .

Este es solo el primer paso y aún habrá margen de mejora, como un modo estricto que obligue a TS a funcionar como en Java, es decir, obligar al usuario a utilizar un método arriesgado (un método que puede arrojar algo) dentro de un try bloque. Y si el codificador no quiere hacerlo, entonces marcaría explícitamente la función como function example throw ErrorType { ... } para escalar la responsabilidad de manejar los errores.

Por último, pero no menos importante: evita perder un error

Se puede arrojar cualquier cosa, no solo un error o incluso una instancia de un objeto. Lo que significa que lo siguiente es válido

try {
    if (Math.random() > 0.5) {
        throw 0
    } else {
        throw new Error()
    }
}
catch (error) { // error can be a number or an object of type Error
    if (typeof error === "number") {
        alert("silly number")
    }

    if (error instanceof Error) {
        alert("error")
    }
}

Saber que el error podría ser del tipo number | Error sería increíblemente útil. Sin embargo, para evitar olvidarse de manejar un posible tipo de error, no es realmente la mejor idea usar bloques separados if / else sin un conjunto estricto de posibles resultados.
Sin embargo, un caso de cambio haría esto mucho mejor, ya que podemos recibir una advertencia si olvidamos hacer coincidir un caso específico (que se recurriría a la cláusula predeterminada). No podemos (a menos que hagamos algo hackish) cambiar el tipo de instancia de un objeto, e incluso si pudiéramos, podemos arrojar cualquier cosa (no solo un objeto sino también un valor booleano, una cadena y un número que no tienen "instancia"). Sin embargo, podemos usar el constructor de la instancia para averiguar de qué tipo es. Ahora podemos reescribir el código anterior de la siguiente manera:

try {
    if (Math.random() > 0.5) {
        throw 0
    } else {
        throw new Error()
    }
}
catch (error) { // error can be a Number or an object of type `Error`
    switch (error.constructor){
        case Number: alert("silly number"); break;
        case Error: alert("error"); break;
    }
}

Horray... el único problema que queda es que TS no escribe error.constructor y, por lo tanto, no hay forma de reducir el caso del interruptor (¿todavía?), Si lo hiciera, tendríamos un lenguaje de error escrito seguro para js.

Por favor comente si necesita más comentarios

¿Fue útil esta página
0 / 5 - 0 calificaciones