Typescript: Suggestion : clause "throws" et clause catch typée

Créé le 29 déc. 2016  ·  135Commentaires  ·  Source: microsoft/TypeScript

Le système de type tapuscrit est utile dans la plupart des cas, mais il ne peut pas être utilisé lors de la gestion des exceptions.
Par example:

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

Le problème ici est double (sans regarder à travers le code):

  1. Lors de l'utilisation de cette fonction, il n'y a aucun moyen de savoir qu'elle pourrait générer une erreur
  2. Il n'est pas clair quel(s) type(s) d'erreur va être

Dans de nombreux scénarios, ce n'est pas vraiment un problème, mais savoir si une fonction/méthode peut lever une exception peut être très utile dans différents scénarios, en particulier lors de l'utilisation de différentes bibliothèques.

En introduisant une exception vérifiée (facultative), le système de type peut être utilisé pour la gestion des exceptions.
Je sais que les exceptions vérifiées ne sont pas convenues (par exemple Anders Hejlsberg ), mais en les rendant facultatives (et peut-être déduites? Plus tard), cela ajoute simplement la possibilité d'ajouter plus d'informations sur le code qui peuvent aider les développeurs, les outils et Documentation.
Cela permettra également une meilleure utilisation des erreurs personnalisées significatives pour les grands projets.

Comme toutes les erreurs d'exécution javascript sont de type Error (ou des types d'extension tels que TypeError ), le type réel d'une fonction sera toujours type | Error .

La grammaire est simple, une définition de fonction peut se terminer par une clause throws suivie d'un type :

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

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

Lors de la capture des exceptions, la syntaxe est la même avec la possibilité de déclarer le(s) type(s) de l'erreur :
catch(e: string | Error) { ... }

Exemples:

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

Ici, il est clair que la fonction peut générer une erreur et que l'erreur sera une chaîne, et donc lors de l'appel de cette méthode, le développeur (et le compilateur/IDE) en est conscient et peut mieux le gérer.
Alors:

fn(0);

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

Compile sans erreur, mais :

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

Échec de la compilation car number n'est pas string .

Flux de contrôle et inférence de type d'erreur

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

Jette string .

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

    fn(num);
}

Jette MyError | string .
Pourtant:

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

Jette seulement MyError .

Awaiting More Feedback Suggestion

Commentaire le plus utile

@aleksey-bykov

Vous suggérez de ne pas utiliser du tout throw dans mon code et d'envelopper à la place les résultats (dans des fonctions susceptibles d'erreur).
Cette approche présente quelques inconvénients :

  • Cet emballage crée plus de code
  • Cela nécessite que toute la chaîne de fonctions invoquées renvoie cette valeur enveloppée (ou erreur) ou bien la fonction qui obtient Tried<> ne peut pas choisir d'ignorer l'erreur.
  • Ce n'est pas une norme, des bibliothèques tierces et les erreurs natives js génèrent des erreurs

L'ajout throws permettra aux développeurs qui choisissent de gérer les erreurs de leur code, des 3èmes bibliothèques et du js natif.
Comme la suggestion demande également une déduction d'erreur, tous les fichiers de définition générés peuvent inclure la clause throws .
Il sera très pratique de savoir quelles erreurs une fonction pourrait générer directement à partir du fichier de définition au lieu de l'état actuel où vous devez accéder à la documentation, par exemple pour savoir quelle erreur JSON.parse pourrait générer Je dois y aller à la page MDN et lisez ceci :

Lève une exception SyntaxError si la chaîne à analyser n'est pas un JSON valide

Et c'est le bon cas lorsque l'erreur est documentée.

Tous les 135 commentaires

Juste pour clarifier - l'une des idées ici n'est pas de forcer les utilisateurs à intercepter l'exception, mais plutôt de mieux déduire le type d'une variable de clause catch ?

@DanielRosenwasser
Oui, les utilisateurs ne seront pas obligés d'intercepter les exceptions, donc cela convient au compilateur (au moment de l'exécution, l'erreur est bien sûr levée):

function fn() {
    throw "error";
}

fn();

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

Mais cela donnera aux développeurs un moyen d'exprimer quelles exceptions peuvent être levées (ce serait génial d'avoir cela lors de l'utilisation d'autres fichiers de bibliothèques .d.ts ) et ensuite de faire en sorte que le type de compilateur garde les types d'exception à l'intérieur de la clause catch.

en quoi un lancer coché est-il différent 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);
}

à la place de

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

@aleksey-bykov

Vous suggérez de ne pas utiliser du tout throw dans mon code et d'envelopper à la place les résultats (dans des fonctions susceptibles d'erreur).
Cette approche présente quelques inconvénients :

  • Cet emballage crée plus de code
  • Cela nécessite que toute la chaîne de fonctions invoquées renvoie cette valeur enveloppée (ou erreur) ou bien la fonction qui obtient Tried<> ne peut pas choisir d'ignorer l'erreur.
  • Ce n'est pas une norme, des bibliothèques tierces et les erreurs natives js génèrent des erreurs

L'ajout throws permettra aux développeurs qui choisissent de gérer les erreurs de leur code, des 3èmes bibliothèques et du js natif.
Comme la suggestion demande également une déduction d'erreur, tous les fichiers de définition générés peuvent inclure la clause throws .
Il sera très pratique de savoir quelles erreurs une fonction pourrait générer directement à partir du fichier de définition au lieu de l'état actuel où vous devez accéder à la documentation, par exemple pour savoir quelle erreur JSON.parse pourrait générer Je dois y aller à la page MDN et lisez ceci :

Lève une exception SyntaxError si la chaîne à analyser n'est pas un JSON valide

Et c'est le bon cas lorsque l'erreur est documentée.

Et c'est le bon cas lorsque l'erreur est documentée.

existe-t-il un moyen fiable en javascript de distinguer SyntaxError de Error?

  • oui, c'est plus du code, mais comme une mauvaise situation est représentée dans un objet, elle peut être transmise pour être traitée, supprimée, stockée ou transformée en un résultat valide comme n'importe quelle autre valeur

  • vous pouvez ignorer essayé en renvoyant également essayé, essayé peut être considéré comme une monade, recherchez des calculs monadiques

    function mightFail(): Tried<number, string> {
    }
    function mightFailToo(): Tried<number, string> {
        const tried = mightFail();
        if (isSuccess(tried))  { 
             return successFrom(tried.result * 2);
        } else {
             return tried;
        }
    }
    
  • c'est assez standard pour votre code, quand il s'agit de bibliothèques tierces qui lancent une exception, cela signifie généralement un gameover pour vous, car il est presque impossible de récupérer de manière fiable à partir d'une exception, la raison en est qu'elle peut être lancée de n'importe où dans le code le terminant à une position arbitraire et laissant son état interne incomplet ou corrompu

  • il n'y a pas de support pour les exceptions vérifiées à partir de l'exécution JavaScript, et je crains qu'il ne puisse pas être implémenté uniquement dans le tapuscrit

à part cela, coder une exception en tant que cas de résultat spécial est une pratique très courante dans le monde FP

alors que diviser un résultat possible en 2 parties :

  • un délivré par la déclaration de retour et
  • un autre livré par lancer

semble une difficulté inventée

à mon avis, lancer est bon pour échouer rapidement et fort quand vous ne pouvez rien y faire, les résultats explicitement codés sont bons pour tout ce qui implique une situation mauvaise mais attendue dont vous pouvez vous remettre

envisager:

// 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

Mon point avec JSON.parse pourrait lancer SyntaxError est que je dois rechercher la fonction dans les docs juste pour savoir qu'elle pourrait lancer, et il serait plus facile de voir cela dans le .d.ts .
Et oui, vous pouvez savoir que c'est SyntaxError en utilisant instanceof .

Vous pouvez représenter la même mauvaise situation en lançant une erreur.
Vous pouvez créer votre propre classe d'erreur qui étend Error et y mettre toutes les données pertinentes dont vous avez besoin.
Vous obtenez la même chose avec moins de code.

Parfois, vous avez une longue chaîne d'invocations de fonctions et vous souhaiterez peut-être traiter certaines des erreurs à différents niveaux de la chaîne.
Il sera assez ennuyeux de toujours utiliser des résultats encapsulés (monades).
Sans oublier qu'encore une fois, d'autres bibliothèques et erreurs natives peuvent être générées de toute façon, vous pouvez donc finir par utiliser à la fois des monades et try/catch.

Je ne suis pas d'accord avec vous, dans de nombreux cas, vous pouvez récupérer des erreurs lancées, et si le langage vous permet de mieux l'exprimer, il sera plus facile de le faire.

Comme avec beaucoup de choses en tapuscrit, le manque de support de la fonctionnalité en javascript n'est pas un problème.
Cette:

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

Fonctionnera comme prévu en javascript, juste sans l'annotation de type.

L'utilisation throw suffit à exprimer ce que vous dites : si l'opération a réussi, renvoyez la valeur, sinon génère une erreur.
L'utilisateur de cette fonction décidera alors s'il veut traiter les éventuelles erreurs ou les ignorer.
Vous ne pouvez traiter que les erreurs que vous avez lancées vous-même et ignorer celles qui sont de tiers par exemple.

si nous parlons de navigateurs instanceof n'est bon que pour les choses qui proviennent de la même fenêtre/du même document, essayez-le :

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

donc quand tu fais :

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

tu ne l'attraperas pas

un autre problème avec des exceptions qu'ils pourraient glisser dans votre code bien au-delà de l'endroit où vous vous attendez à ce qu'ils se produisent

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

outre que instanceof est vulnérable à l'héritage du prototype, vous devez donc redoubler de prudence pour toujours vérifier par rapport à l'ancêtre 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 Les erreurs de threading explicites comme vous le suggérez dans les structures monadiques sont une tâche assez difficile et décourageante. Cela demande beaucoup d'efforts, rend le code difficile à comprendre et nécessite un support de langage / une émission basée sur le type pour être sur le point d'être supportable. Ceci est un commentaire venant de quelqu'un qui met beaucoup d'efforts pour populariser Haskell et FP dans son ensemble.

C'est une alternative de travail, en particulier pour les passionnés (moi y compris), mais je ne pense pas que ce soit une option viable pour un public plus large.

En fait, ma principale préoccupation ici est que les gens commenceront à sous-classer Error. Je pense que c'est un modèle terrible. Plus généralement, tout ce qui promeut l'utilisation de l'opérateur instanceof ne fera que créer une confusion supplémentaire autour des classes.

Ceci est un commentaire venant de quelqu'un qui met beaucoup d'efforts pour populariser Haskell et FP dans son ensemble.

je pense vraiment que cela devrait être poussé plus fort vers le public, pas tant qu'il n'est pas digéré et demandé plus, pouvons-nous avoir un meilleur support FP dans la langue

et ce n'est pas aussi intimidant que vous le pensez, à condition que tous les combinateurs soient déjà écrits, utilisez-les simplement pour créer un flux de données, comme nous le faisons dans notre projet, mais je suis d'accord que TS aurait pu mieux le supporter : #2319

Les transformateurs Monad sont un vrai PITA. Vous avez besoin de levage, de hissage et de course sélective assez souvent. Le résultat final est un code difficilement compréhensible et une barrière d'entrée beaucoup plus élevée que nécessaire. Tous les combinateurs et fonctions de levage (qui fournissent le boxing/unboxing obligatoire) ne sont que du bruit qui vous distrait du problème à résoudre. Je crois qu'être explicite sur l'état, les effets, etc. est une bonne chose, mais je ne pense pas que nous ayons encore trouvé un emballage / abstraction pratique. Jusqu'à ce que nous le trouvions, soutenir les modèles de programmation traditionnels semble être la voie à suivre sans s'arrêter pour expérimenter et explorer en même temps.

PS : Je pense que nous avons besoin de plus que d'opérateurs personnalisés. Les types de type supérieur et certaines sortes de classes de types sont également essentiels pour une bibliothèque monadique pratique. Parmi eux, je classerais HKT en premier et les classes de type en deuxième position. Cela dit, je pense que TypeScript n'est pas le langage pour pratiquer de tels concepts. Jouer autour - oui, mais sa philosophie et ses racines sont fondamentalement éloignées pour une bonne intégration transparente.

Revenons à la question OP - instanceof est un opérateur dangereux à utiliser. Cependant, les exceptions explicites ne sont pas limitées à Error . Vous pouvez également lancer vos propres erreurs ADT ou POJO personnalisées. La fonctionnalité proposée peut être très utile et, bien sûr, peut également être mal utilisée. En tout cas, cela rend les fonctions plus transparentes, ce qui est sans aucun doute une bonne chose. Dans l'ensemble, je suis 50/50 dessus :)

@aleksey-bykov

Les développeurs doivent être conscients des différents problèmes de js que vous avez décrits, après tout, l'ajout throws à dactylographie n'introduit rien de nouveau dans js, il ne fait que donner à dactylographie en tant que langage la possibilité d'exprimer un comportement js existant.

Le fait que les bibliothèques tierces puissent générer des erreurs est exactement ce que je veux dire.
Si leurs fichiers de définition devaient inclure cela, j'aurais un moyen de le savoir.

@aluanhaddad
Pourquoi est-ce un motif terrible d'étendre Error ?

@gcnew
Quant à instanceof , ce n'était qu'un exemple, je peux toujours jeter des objets réguliers qui ont des types différents, puis utiliser des gardes de type pour les différencier.
Il appartiendra au développeur de décider quel type d'erreurs il souhaite lancer, et c'est probablement déjà le cas, mais actuellement il n'y a aucun moyen d'exprimer cela, c'est ce que cette suggestion veut résoudre.

@nitzantomer Les classes natives de sous-classement ( Error , Array , RegExp , etc.) n'étaient pas prises en charge dans les anciennes versions d'ECMAScript (avant ES6). L'émission de bas niveau pour ces classes donne des résultats inattendus (le meilleur effort est fait mais c'est aussi loin que l'on peut aller) et est la raison de nombreux problèmes enregistrés quotidiennement. En règle générale, ne sous-classez pas les natifs à moins que vous ne cibliez les versions récentes d'ECMAScript et que vous ne sachiez vraiment ce que vous faites.

@gcnew
Oh, j'en suis bien conscient car j'ai passé plus de quelques heures à essayer de comprendre ce qui n'allait pas.
Mais avec la possibilité de le faire maintenant, il ne devrait pas y avoir de raison de ne pas le faire (lorsque vous ciblez es6).

Dans tous les cas, cette suggestion ne suppose pas que l'utilisateur sous-classe la classe Error, c'était juste un exemple.

@nitzantomer Je ne dis pas que la suggestion est limitée à Error . Je viens d'expliquer pourquoi c'est un mauvais modèle de le sous-classer. Dans mon message, j'ai en fait défendu la position selon laquelle des objets personnalisés ou des unions discriminées peuvent également être utilisés.

instanceof est dangereux et considéré comme un anti-modèle même si vous supprimez les spécificités de JavaScript - par exemple Méfiez-vous de l'opérateur instanceof . La raison en est que le compilateur ne peut pas vous protéger contre les bogues introduits par les nouvelles sous-classes. La logique utilisant instanceof est fragile et ne suit pas le principe ouvert/fermé , car elle n'attend qu'une poignée d'options. Même si un caractère générique est ajouté, de nouveaux dérivés sont toujours susceptibles de provoquer des erreurs car ils peuvent casser les hypothèses formulées au moment de la rédaction.

Pour les cas où vous souhaitez faire la distinction entre les alternatives connues, TypeScript a Tagged Unions (également appelées unions discriminées ou types de données algébriques). Le compilateur s'assure que tous les cas sont traités ce qui vous donne de belles garanties. L'inconvénient est que si vous souhaitez ajouter une nouvelle entrée au type, vous devrez parcourir tout le code discriminant et gérer l'option nouvellement ajoutée. L'avantage est qu'un tel code aurait probablement été cassé, mais aurait échoué lors de l'exécution.

J'ai juste donné une seconde réflexion à cette proposition et je suis devenu contre. La raison en est que si des déclarations throws étaient présentes sur les signatures mais n'étaient pas appliquées, elles peuvent déjà être gérées par des commentaires de documentation. En cas d'application, je partage le sentiment qu'ils deviendraient irritants et avalés rapidement car JavaScript ne dispose pas du mécanisme de Java pour les clauses catch typées. L'utilisation d'exceptions (en particulier en tant que flux de contrôle) n'a jamais été une pratique établie. Tout cela m'amène à comprendre que les exceptions vérifiées apportent trop peu, alors que des moyens meilleurs et actuellement plus courants de représenter l'échec sont disponibles (par exemple, le retour d'union).

@gcnew
C'est comme ça que ça se passe en C #, le problème est que les docs ne sont pas aussi standard en tapuscrit.
Je ne me souviens pas être tombé sur un fichier de définition bien documenté. Les différents fichiers lib.d.ts contiennent des commentaires, mais ceux-ci ne contiennent pas d'erreurs générées (à une exception près : lib.es6.d.ts a un throws dans Date[Symbol.toPrimitive](hint: string) ).

En outre, cette suggestion prend en compte l'inférence d'erreur, ce qui ne se produira pas si des erreurs proviennent de commentaires de documentation. Avec les exceptions vérifiées inférées, le développeur n'aura même pas besoin de spécifier la clause throws , le compilateur la déduira automatiquement et l'utilisera pour la compilation et l'ajoutera au fichier de définition résultant.

Je conviens que l'application de la gestion des erreurs n'est pas une bonne chose, mais le fait d'avoir cette fonctionnalité ajoutera simplement plus d'informations qui pourront ensuite être utilisées par ceux qui le souhaitent.
Le problème avec :

... il existe des façons meilleures et actuellement plus courantes de représenter l'échec

Est-ce qu'il n'y a pas de façon standard de le faire.
Vous pouvez utiliser union return, @aleksey-bykov utilisera Tried<> , et un développeur d'une autre bibliothèque tierce fera quelque chose de complètement différent.
Lancer des erreurs est une norme dans tous les langages (js, java, c# ...) et comme cela fait partie du système et non une solution de contournement, il devrait (à mon avis) avoir une meilleure gestion en tapuscrit, et une preuve en est le nombre des problèmes que j'ai vus ici au fil du temps qui demandent une annotation de type dans la clause catch .

J'aimerais avoir des informations dans l'info-bulle de VS si une fonction (ou une fonction appelée) peut lancer. Pour les fichiers *.d.ts , nous avons probablement besoin d'un faux paramètre comme celui-ci depuis TS2.0.

@HolgerJeromin
Pourquoi serait-il nécessaire?

voici une question simple, quelle signature doit être déduite pour dontCare dans le code ci-dessous ?

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

function dontCare() {
   return mightThrow();
}

d'après ce que vous avez dit dans votre proposition, il devrait être

function dontCare(): void throws string {

je dis que ce devrait être une erreur de type car une exception vérifiée n'a pas été correctement gérée

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

pourquoi donc?

car sinon il y a de très fortes chances que l'état de l'appelant immédiat soit corrompu :

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

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

si vous laissez passer une exception, vous ne pouvez pas en déduire qu'elle est cochée, car le contrat de comportement de keepAllValues serait violé de cette façon (toutes les valeurs n'ont pas été conservées malgré l'intention d'origine)

le seul moyen sûr est de les attraper immédiatement et de les relancer explicitement

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

sinon, bien que les appelants sachent ce qui peut être trown, vous ne pouvez pas leur donner la garantie qu'il est sûr de continuer en utilisant le code qui vient de lancer

il n'y a donc pas de propagation automatique de contrat d'exception vérifiée

et corrigez-moi si je me trompe, c'est exactement ce que fait Java, que vous avez mentionné comme exemple plus tôt

@aleksey-bykov
Cette:

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

function dontCare() {
   return mightThrow();
}

Signifie que mightThrow et dontCare sont déduits de throws string , cependant :

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

N'aura pas de clause throw car l'erreur a été traitée.
Cette:

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

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

Aura throws MyErrorType .

Quant à votre exemple keepAllValues , je ne suis pas sûr de ce que vous voulez dire, dans votre exemple :

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

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

MyClass.keepAllValues sera déduit comme throws string car mightThrow pourrait lancer un string et cette erreur n'a pas été gérée.

Quant à votre exemple keepAllValues , je ne suis pas sûr de ce que vous voulez dire

Je voulais dire que les exceptions non gérées par mightThrow interrompent keepAllValues et le font finir au milieu de ce qu'il faisait en laissant son état corrompu. C'est un problème. Ce que vous suggérez, c'est de fermer les yeux sur ce problème et de prétendre que ce n'est pas grave. Ce que je suggère est de résoudre ce problème en exigeant que toutes les exceptions vérifiées soient immédiatement gérées et renvoyées explicitement. De cette façon, il n'y a aucun moyen de corrompre l'État sans le vouloir . Et bien qu'il puisse toujours être corrompu si vous le choisissez, cela nécessiterait un codage délibéré.

Pensez-y, il y a 2 façons de traiter les exceptions :

  • les débloquer, ce qui conduit à un crash, si le crash est ce que vous voulez alors nous sommes bien ici
  • si vous ne voulez pas de plantage, vous avez besoin de conseils sur le type d'exception que vous devez rechercher, et c'est là que votre proposition entre en jeu : les exceptions vérifiées - toutes explicitement répertoriées, afin que vous puissiez toutes les gérer et ne pas rien ne manque

maintenant, si nous décidons d'aller avec les exceptions vérifiées qui sont correctement gérées et empêchent un plantage, nous devons exclure une situation où nous traitons une exception provenant de plusieurs couches profondes de l'endroit où vous l'attrapez :

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
}

l'exemple ci-dessus apporte un cas intéressant à considérer, quelle serait la signature déduite de :

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

un autre cas intéressant

// 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
Donc, vous proposez que toutes les erreurs doivent être traitées comme c'est le cas avec Java ?
Je ne suis pas fan de ça (même si je viens de Java et que je l'aime toujours) parce que js/ts sont beaucoup plus dynamiques et leurs utilisateurs y sont habitués.
Il peut s'agir d'un indicateur qui vous oblige à gérer les erreurs si vous l'incluez lors de la compilation (comme strictNullChecks ).

Ma suggestion n'est pas là pour résoudre les exceptions non gérées, le code que vous avez publié se cassera maintenant sans cette fonctionnalité implémentée, et il se cassera également en js.
Ma suggestion vous permet simplement, en tant que développeur, d'être plus conscient des différentes erreurs qui peuvent être générées, c'est toujours à vous de les gérer ou de les ignorer.

Quant au problème de la division par 0, il ne génère pas d'erreur :

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

plus conscient des différentes erreurs qui pourraient être lancées

il ne sert à rien de le faire à moins qu'ils ne puissent y faire face, la proposition actuelle n'est pas viable à cause des cas que j'ai énumérés

Donc, vous proposez que toutes les erreurs doivent être traitées comme c'est le cas avec Java ?

oui, c'est ce que cela signifie d'avoir vérifié les exceptions

@aleksey-bykov
Je ne vois pas pourquoi l'un des cas que vous avez énumérés rend cette proposition inviable.

Il n'y a aucun problème avec la gestion d'une erreur qui a été lancée tout au long de la chaîne d'invocation, même si j'utilise une fonction qui a été déduite de lancer DivisionByZero (quel que soit l'endroit où elle a été lancée), je peux choisir de le gérer .
Je peux essayer de le réessayer avec des arguments différents, je peux montrer à l'utilisateur un message indiquant que quelque chose s'est mal passé, je peux consigner ce problème afin de pouvoir modifier ultérieurement mon code pour le gérer (si cela se produit souvent).

Encore une fois, cette proposition ne change rien à l'exécution, donc tout ce qui a fonctionné continuera à fonctionner comme avant.
La seule différence est que j'aurai plus d'informations sur les erreurs qui pourraient être levées.

je vois ce que vous dites, rien ne va être changé lors de l'exécution de javascript, mais votre message ici est de donner aux utilisateurs l'illusion qu'ils savent ce qu'ils font en gérant une exception provenant de 20 couches ci-dessous avec la même confiance que ils géreraient une exception immédiate

il n'y a tout simplement aucun moyen de résoudre un problème qui s'est produit 20 couches plus bas

vous pouvez le consigner, bien sûr, comme n'importe quelle exception non vérifiée, mais vous ne pouvez pas le réparer

donc c'est un mensonge en général, il y a assez de mensonges dans TS, ne confondons pas encore plus les gens

@aleksey-bykov
Ce que vous décrivez existe dans toutes les langues qui prennent en charge les exceptions.
Personne n'a dit que la capture d'une exception résoudrait le problème, mais cela vous permettrait de le gérer avec élégance.

Savoir quelles erreurs peuvent être générées lors de l'appel d'une fonction aidera les développeurs à faire la distinction entre les erreurs qu'ils peuvent gérer et celles qu'ils ne peuvent pas.

À l'heure actuelle, les développeurs ne savent peut-être pas que l'utilisation JSON.parse peut générer une erreur, mais si cela faisait partie de lib.d.ts et que l'IDE le lui faisait savoir (par exemple), il choisirait peut-être de gérer cette affaire.

vous ne pouvez pas gérer un problème survenu 20 couches en dessous avec élégance, car l'état interne est corrompu dans 19 couches et vous ne pouvez pas y aller car l'état est privé

pour être constructif : ce que je suggère, c'est d'exiger que les utilisateurs gèrent immédiatement les exceptions vérifiées et les renvoient explicitement, de cette façon, nous excluons toute confusion involontaire et séparons les exceptions vérifiées des non vérifiées :

  • exception vérifiée : s'est produit à portée immédiate et doit être géré, de cette façon, il est garanti que l'état n'est pas corrompu et qu'il est sûr de continuer
  • exception non vérifiée : s'est produit à portée immédiate ou beaucoup plus bas, il ne peut pas être géré car l'état était corrompu, vous pouvez le consigner ou procéder à vos risques et périls

SyntaxError dans JSON.parse doit être déclaré comme une exception vérifiée

@aleksey-bykov

Je ne vois pas pourquoi il est nécessaire d'obliger les développeurs à faire quelque chose qu'ils ne souhaitent pas faire, quelque chose qu'ils n'ont pas fait jusqu'à présent.

Voici un exemple :
J'ai un client Web dans lequel l'utilisateur peut écrire/coller des données json, puis cliquer sur un bouton.
L'application prend cette entrée et la transmet à une bibliothèque tierce qui analyse en quelque sorte ce json et renvoie le json avec les différents types de valeurs (chaîne, nombre, booléen, tableau, etc.).
Si cette bibliothèque tierce génère un SyntaxError , je peux récupérer : informez l'utilisateur que son entrée n'est pas valide et qu'il doit réessayer.

En sachant quelles erreurs peuvent être générées lors de l'appel d'une fonction, le développeur peut décider ce qu'il peut/souhaite gérer et ce qu'il ne peut pas.
Peu importe la profondeur de la chaîne dans laquelle l'erreur a été générée.

écoute tu n'as pas l'air de comprendre ce que je dis, on tourne en rond

en laissant SyntaxError jeté depuis une bibliothèque tierce, vous exposez votre utilisateur aux détails d'implémentation de votre propre code qui sont censés être encapsulés

fondamentalement, vous dites, hé, ce n'est pas mon code qui ne fonctionne pas, c'est cette bibliothèque stupide que j'ai trouvée sur Internet et utilisée, donc si vous avez un problème avec, traitez avec cette bibliothèque tierce, pas moi, je je viens de dire ce qu'on m'a demandé

et il n'y a aucune garantie que vous puissiez toujours utiliser l'instance de cette 3ème bibliothèque après cette SyntaxError, il est de votre responsabilité de fournir des garanties à l'utilisateur, par exemple en réinstanciant le contrôle tiers après qu'il ait jeté

En bout de ligne, vous devez être en charge de la gestion des exceptions internes ( pas toutes, seulement celles cochées, je vous en prie )

Je comprends ce que vous dites, mais je ne suis pas d'accord avec cela.
Tu as raison, c'est essentiellement ce que je dis.
Si j'ai utilisé une bibliothèque tierce qui génère une erreur, je peux choisir de la traiter ou de l'ignorer et de laisser l'utilisateur de mon code le gérer.
Il y a de nombreuses raisons de le faire, par exemple la bibliothèque que j'écris est indépendante de l'interface utilisateur, donc je ne peux pas informer l'utilisateur que quelque chose ne va pas, mais celui qui utilise ma bibliothèque peut gérer les erreurs qui sont générées lors de l'utilisation my lib et les gérer en interagissant avec l'utilisateur.

Si une bibliothèque se retrouve avec un état corrompu lors de son lancement, elle doit probablement le documenter.
Si j'utilise ensuite une telle bibliothèque et que, par conséquent, une erreur s'y trouve, mon état est corrompu, je dois le documenter.

En bout de ligne :
Cette suggestion vient offrir plus d'informations sur les erreurs générées.
Cela ne devrait pas obliger les développeurs à faire les choses différemment, mais simplement leur faciliter la gestion des erreurs s'ils le souhaitent.

vous pouvez être en désaccord, c'est bien, ne les appelons simplement pas des exceptions vérifiées s'il vous plaît, parce que la façon dont vous le dites n'est pas ce que sont les exceptions vérifiées

appelons-les des exceptions répertoriées ou révélées , car tout ce qui vous importe est d'en informer les développeurs

@aleksey-bykov
C'est normal, le nom a changé.

@aleksey-bykov

vous ne pouvez pas gérer un problème survenu 20 couches en dessous avec élégance, car l'état interne est corrompu dans 19 couches et vous ne pouvez pas y aller car l'état est privé

Non, vous ne pouvez pas corriger l'état interne, mais vous pouvez certainement corriger l'état local, et c'est exactement le but de le gérer ici et non plus profondément dans la pile.

Si votre argument est qu'il n'y a aucun moyen d'être certain de l'état dans lequel se trouvent certaines valeurs modifiables partagées lors de la gestion de l'exception, alors c'est un argument contre la programmation impérative, et non limité à cette proposition.

si chaque couche est tenue de prendre la responsabilité de réagir à une exception provenant immédiatement d'une couche inférieure, il y a de bien meilleures chances de réussite de la récupération, c'est l'idée derrière les exceptions vérifiées telles que je les vois

pour le dire autrement, les exceptions provenant de plus d'un niveau en dessous est une phrase, il est trop tard pour faire autre chose que de ré-instancier toute l'infrastructure à partir de zéro (si vous êtes assez chanceux, il n'y a pas de restes globaux que vous pouvez ' t atteindre)

la proposition telle qu'énoncée est généralement inutile, car il n'y a aucun moyen fiable de réagir à la connaissance de quelque chose de mal qui s'est passé hors de votre portée

C'est bien. FWIW : Je pense que si elle est ajoutée, elle devrait être requise par défaut pour gérer les méthodes de lancer ou marquer votre méthode comme lançant également. Sinon, c'est juste de la documentation à peu près.

@agonzalezjr
Je pense que, comme la plupart des fonctionnalités de dactylographie, vous devriez également pouvoir vous inscrire à cette fonctionnalité.
Tout comme il n'est pas obligatoire d'ajouter des types, il ne devrait pas être indispensable de lancer/attraper.

Il devrait probablement y avoir un drapeau pour en faire un must, comme --onlyCheckedExceptions .

Dans tous les cas, cette fonctionnalité sera également utilisée pour déduire/valider les types d'exceptions levées, donc pas seulement pour la documentation.

@nitzantomer

Voici un exemple :
J'ai un client Web dans lequel l'utilisateur peut écrire/coller des données json, puis cliquer sur un bouton.
L'application prend cette entrée et la transmet à une bibliothèque tierce qui analyse en quelque sorte ce json et renvoie le json avec les différents types de valeurs (chaîne, nombre, booléen, tableau, etc.).
Si cette bibliothèque tierce génère une SyntaxError, je peux récupérer : informez l'utilisateur que son entrée n'est pas valide et qu'il doit réessayer.

C'est certainement un domaine où toute l'idée d'exceptions vérifiées devient trouble. C'est aussi là que la définition de _situation exceptionnelle_ devient floue.
Le programme dans votre exemple serait un argument pour que JSON.parse soit déclaré comme levant une exception vérifiée.
Mais que se passe-t-il si le programme est un client HTTP et appelle JSON.parse en fonction de la valeur d'un en-tête attaché à une réponse HTTP contenant un corps mal formé ? Il n'y a rien de significatif que le programme puisse faire pour récupérer, tout ce qu'il peut faire est de relancer.
Je dirais que c'est un argument contre la déclaration de JSON.parse comme vérifié.

Tout dépend du cas d'utilisation.

Je comprends que vous proposez que ce soit sous un drapeau mais imaginons que je veuille utiliser cette fonctionnalité donc j'ai activé le drapeau. Selon le type de programme que j'écris, cela peut m'aider ou m'entraver.

Même le classique java.io.FileNotFoundException en est un exemple. Il est coché mais le programme peut-il récupérer ? Cela dépend vraiment de ce que le fichier manquant signifie pour l'appelant, pas pour l'appelé.

@aluanhaddad

Cette suggestion ne propose pas d'ajouter de nouvelles fonctionnalités, seulement d'ajouter un moyen d'exprimer en tapuscrit quelque chose qui existe déjà en javascript.
Des erreurs sont lancées, mais actuellement le script dactylographié n'a aucun moyen de les déclarer (quand on les lance ou qu'on les attrape).

En ce qui concerne votre exemple, en captant l'erreur, le programme peut échouer "gracieusement" (par exemple en montrant à l'utilisateur un message "quelque chose s'est mal passé") en captant cette erreur, ou il peut l'ignorer, selon le programme/développeur.
Si l'état des programmes peut être affecté par cette erreur, sa gestion peut conserver un état valide au lieu d'un état cassé.

Dans tous les cas, le développeur doit décider s'il peut ou non récupérer d'une erreur générée.
C'est aussi à lui de décider ce que signifie récupérer, par exemple si j'écris ce client http pour qu'il soit utilisé comme bibliothèque tierce, je souhaiterais peut-être que toutes les erreurs générées par ma bibliothèque soient du même type :

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

Maintenant, dans ma bibliothèque, lorsque j'analyse la réponse à l'aide JSON.parse , je veux intercepter une erreur générée, puis générer ma propre erreur :

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

Si cette fonctionnalité est implémentée, il me sera facile de déclarer ce comportement et les utilisateurs de ma bibliothèque comprendront clairement comment cela fonctionne et échoue.

Cette suggestion ne propose pas d'ajouter de nouvelles fonctionnalités, seulement d'ajouter un moyen d'exprimer en tapuscrit quelque chose qui existe déjà en javascript.

Je sais. Je parle des erreurs que TypeScript émettrait dans le cadre de cette proposition.
Mon hypothèse était que cette proposition impliquait une distinction entre les spécificateurs d'exception vérifiés et non vérifiés (inférés ou explicites), encore une fois uniquement à des fins de vérification de type.

@aluanhaddad

Ce que vous avez dit dans le commentaire précédent :

Mais que se passe-t-il si le programme est un client HTTP et appelle JSON.parse en fonction de la valeur d'un en-tête attaché à une réponse HTTP contenant un corps mal formé ? Il n'y a rien de significatif que le programme puisse faire pour récupérer, tout ce qu'il peut faire est de relancer.

S'applique de même pour renvoyer un null lorsque ma fonction est déclarée pour renvoyer un résultat.
Si le développeur choisit d'utiliser strictNullChecks alors vous pouvez dire exactement la même chose si la fonction renvoie un null (au lieu de lancer) alors dans le même scénario "il n'y a rien de significatif que le programme puisse faire s'en remettre".

Mais même sans utiliser un indicateur onlyCheckedExceptions , cette fonctionnalité est toujours utile car le compilateur se plaindra par exemple si j'essaie d'attraper l'erreur comme un string lorsque la fonction est déclarée pour ne lancer que Error .

Bonne idée, ce serait utile mais pas strict/type sûr car il n'y a aucun moyen de savoir ce que les appels imbriqués pourraient vous lancer.

Cela signifie que si j'ai une fonction qui pourrait lever une exception de type A, mais à l'intérieur, j'appelle une fonction imbriquée et ne la mets pas dans try catch - elle va lever son exception de type B à mon appelant.
Ainsi, si l'appelant n'attend que des exceptions de type A, il n'y a aucune garantie qu'il n'obtiendra pas d'autres types d'exceptions imbriquées.

(le fil est trop long donc - désolé si j'ai raté ce commentaire)

@shaipetel
La proposition indique que le compilateur déduira les types d'erreurs non gérées et les ajoutera à la signature de la fonction/méthode.
Donc, dans le cas que vous avez décrit, votre fonction lancera A | B au cas où B n'aurait pas été géré.

Oh je vois. Il explorera toutes les méthodes que j'appelle et collectera tous les types d'exceptions possibles ?
J'aimerais que cela se produise, si c'est possible. Vous voyez, un développeur peut toujours avoir une exception inattendue qui ne sera pas déclarée, auquel cas un "objet non défini sur l'instance" ou "divisé par 0" ou des exceptions similaires sont toujours possibles presque à partir de n'importe quelle fonction.
À mon humble avis, cela aurait été mieux géré comme en C # où toutes les exceptions héritent d'une classe de base qui a un message et de ne pas autoriser du tout le lancement de texte déballé ou d'autres objets. Si vous avez une classe de base et un héritage, vous pouvez cascader vos captures et gérer votre erreur attendue dans un bloc, et d'autres inattendues dans un autre.

@shaipetel
En javascript, toutes les erreurs sont basées sur la classe Error , mais vous n'êtes pas limité à lancer des erreurs, vous pouvez lancer n'importe quoi :

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

Oui, je sais comment fonctionne JavaScript, nous discutons de TypeScript qui impose d'autres limitations.
J'ai suggéré, une bonne solution à mon humble avis serait de faire en sorte que TypeScript suive la gestion des exceptions qui nécessite que toutes les exceptions levées appartiennent à une classe de base spécifique et ne permettent pas de lancer directement des valeurs non enveloppées.

Ainsi, il n'autorisera pas "lancer 0" ou "lancer 'une erreur'".
Tout comme JavaScript permet beaucoup de choses que TypeScript ne permet pas.

Merci,

@nitzantomer

S'applique de même pour renvoyer un null lorsque ma fonction est déclarée pour renvoyer un résultat.
Si le développeur choisit d'utiliser strictNullChecks, vous pouvez dire exactement la même chose si la fonction renvoie un null (au lieu de lancer), puis dans le même scénario "il n'y a rien de significatif que le programme puisse faire pour récupérer".

Mais même sans utiliser un indicateur onlyCheckedExceptions, cette fonctionnalité est toujours utile car le compilateur se plaindra par exemple si j'essaie d'attraper l'erreur sous forme de chaîne lorsque la fonction est déclarée pour ne lancer qu'une erreur.

Je vois ce que tu dis, c'est logique.

@shaipetel , comme indiqué précédemment dans cette proposition et ailleurs, la sous-classification de fonctions intégrées telles que Error et Array ne fonctionne pas. Cela conduit à un comportement différent selon les temps d'exécution et les cibles de compilation largement utilisés.

La capture de valeurs de divers types de non-erreurs est la manière la plus viable d'envisager la fonctionnalité proposée. En fait, je ne vois pas cela comme un problème, car les mécanismes de propagation des exceptions tirant parti de cette fonctionnalité entraîneraient probablement des erreurs beaucoup plus spécifiques et beaucoup plus utiles que, par exemple, une trace de pile ou d'autres propriétés spécifiques à l'erreur.
L'extension Error n'est pas viable. Il ne sera pas viable tant que tous les objectifs inférieurs à es2015 ne seront plus utilisés.

Si cette proposition conduit indirectement à davantage de sous-classement de la fonction Error , je pense que c'est une mauvaise idée. Une telle objection est complètement distincte de toute objection philosophique concernant l'utilisation d'exceptions pour le flux de contrôle ou la définition de circonstances exceptionnelles. Par conséquent, si cette proposition était adoptée, je m'attendrais à une documentation extrêmement bruyante et directe concernant l'utilisation correcte et la nécessité d'éviter de sous-classer Error .

J'aimerais qu'il y ait une sorte de logique d'assistance pour gérer les exceptions typées, je refactorise beaucoup de code de promesse pour utiliser async/wait, qui ressemble actuellement à :

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

Ensuite, dans le nouveau monde, cela ressemble à ceci:

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

Ce qui est correct mais nécessite plus de code et est légèrement moins lisible à certains égards, donc ce serait formidable s'il y avait une forme de support pour :

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

Ce qui produirait le bit précédent, mais en tant que sucre syntaxique, vous pourriez même le faire en tant que génériques mais c'est légèrement plus méchant :

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

@grofit
bien que j'aimerais que le texte dactylographié appuie ce que vous suggérez, il n'est pas lié à ce problème à mon avis.
Ce que vous suggérez peut même être implémenté sans implémenter ce problème, juste que le compilateur ne pourra pas se plaindre (par exemple) que NotFoundError n'est pas lancé.

Je pensais que c'était une suggestion pour les captures dactylographiées, pas un problème d'aucune sorte, je ne voulais tout simplement pas dupliquer les discussions, j'irai alors le publier dans son propre numéro.

@grofit
Les captures typées font également partie de la demande de fonctionnalité, mais aussi la possibilité d'informer le compilateur du type d'erreurs pouvant être générées à partir des fonctions (et le compilateur pourra également déduire quels sont les types d'erreurs pouvant être générées).

À mon avis, les captures typées peuvent être implémentées sans les autres parties. Ouvrez un nouveau problème, peut-être que l'équipe TS décidera de le marquer comme doublon, je ne sais pas.

@grofit

Ce qui produirait le bit précédent, mais en tant que sucre syntaxique, vous pourriez même le faire en tant que génériques mais c'est légèrement plus méchant :

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

ne fonctionnera pas car cela implique la génération de code basé uniquement sur les types alors que

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

passe la fonction Error dans chaque cas et est probablement implémentée avec instanceof . Un code comme celui-ci est toxique car il encourage l'extension Error , ce qui est une très mauvaise chose.

@nitzantomer Je suis d'accord qu'il s'agit d'un problème distinct. L'OP contient des exemples de types de capture qui n'étendent pas Error .

@aluanhaddad
Pour ce que @grofit demande, vous pouvez alors faire quelque chose comme :

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

Où les isXXX(error) sont des gardes de type de la forme :
function isXXX(error): error is XXX { ... }

@nitzantomer bien sûr, mais les gardes de type ne sont pas le problème. Le problème est

class MyError extends Error {
  myErrorInfo: string;
}

ce qui est problématique, mais a déjà été discuté ici. Je ne souhaite pas insister sur ce point, mais de nombreuses bases de code importantes et bien connues ont été négativement affectées par l'adoption de cette mauvaise pratique. Étendre Error est une mauvaise idée.

@aluanhaddad
J'en suis conscient, c'est pourquoi j'ai proposé un moyen d'accomplir ce que @grofit a demandé mais sans avoir besoin d'étendre Error . Un développeur pourra toujours étendre Error s'il le souhaite, mais le compilateur pourra générer du code js qui n'a pas besoin d'utiliser instanceof

@nitzantomer Je me rends compte que vous êtes au courant, mais je n'ai pas réalisé ce que vous suggériez à @grofit , ce qui semble être une bonne idée.

Je bats juste un cheval mort parce que je n'aime pas avoir affaire à des API qui veulent me faire utiliser de tels modèles. Quoi qu'il en soit, je suis désolé si j'ai pris cette discussion hors sujet.

Y a-t-il eu plus de réflexion dans cette discussion? J'adorerais voir une clause throws en tapuscrit.

@aluanhaddad Vous n'arrêtez pas de dire que l'extension Error est une mauvaise pratique. Pouvez-vous élaborer un peu là-dessus?

Cela a été longuement discuté. Fondamentalement, vous ne pouvez pas étendre de manière fiable les types intégrés. Le comportement varie considérablement selon la combinaison d'environnements et de paramètres --target .

La seule raison pour laquelle j'ai demandé, c'est parce que vos commentaires étaient la première fois que j'en ai entendu parler. Après avoir cherché un peu sur Google, je n'ai trouvé que des articles expliquant comment et pourquoi _devrait_ étendre Error .

Non, il ne travaillera pas. Vous pouvez lire à ce sujet en détail dans https://github.com/Microsoft/TypeScript/issues/12123 et les nombreux problèmes liés.

Cela fonctionnera, mais uniquement dans des "environnements spécifiques", qui deviennent majoritaires au fur et à mesure de l'adaptation d'ES6.

@nitzantomer : Pensez-vous qu'il est possible d'implémenter une vérification TSLint qui informe le développeur lorsqu'une fonction avec @throws est appelée sans try/catch autour ?

@bennyn Je suis un utilisateur de TSLint, mais je n'ai jamais envisagé de créer de nouvelles règles.
Je ne sais vraiment pas si c'est possible (même si je suppose que c'est le cas), et si oui, à quel point c'est facile.

Si vous essayez, veuillez mettre à jour ici, merci.

@shaipetel (et aussi @nitzantomer)

Ré:

Oui, je sais comment fonctionne JavaScript, nous discutons de TypeScript qui impose d'autres limitations.
J'ai suggéré, une bonne solution à mon humble avis serait de faire en sorte que TypeScript suive la gestion des exceptions qui nécessite que toutes les exceptions levées appartiennent à une classe de base spécifique et ne permettent pas de lancer directement des valeurs non enveloppées.
Ainsi, il n'autorisera pas "lancer 0" ou "lancer 'une erreur'".
Tout comme JavaScript permet beaucoup de choses que TypeScript ne permet pas.

Je ne sais pas si c'est ce que vous suggérez @shaipetel mais juste au cas où... Je vous déconseille de laisser Typescript restreindre throw à ne renvoyer que Error s. Les prochaines fonctionnalités de rendu asynchrone de React fonctionnent sous le capot en throw ing Promise s ! (Cela semble étrange, je sais... mais j'ai lu qu'ils ont évalué cela par rapport aux générateurs async / await et yield / yield* pour leur cas d'utilisation et je suis sûr de savoir ce qu'ils font !)

throw ing Error s est je suis sûr que 99% des cas d'utilisation existent, mais pas 100%. Je ne pense pas que Typescript devrait restreindre les utilisateurs à seulement throw ing choses qui étendent une classe de base Error . C'est certainement beaucoup plus avancé, mais throw a d'autres cas d'utilisation que les erreurs/exceptions.

@mikeleonard
Je suis d'accord, il existe de nombreux exemples de code js existant qui lance une variété de types (j'ai vu beaucoup de throw "error message string" ).
La nouvelle fonctionnalité React est un autre bon exemple.

Juste mes deux cents, je convertis le code en async/wait et donc je suis soumis à jeter (en aparté, je déteste les exceptions). Avoir une clause de rejet comme ce problème en discute serait bien à mon avis. Je pense que ce serait aussi utile même si "lancer n'importe quoi" était autorisé. (Aussi, peut-être une option "nothrows" et un compilateur qui par défaut sont "nothrows".)

Cela semble être une extension naturelle pour permettre de taper ce qu'une fonction lance. Les valeurs de retour peuvent éventuellement être saisies dans TS, et pour moi, le lancer n'est qu'un autre type de retour (comme en témoignent toutes les alternatives suggérées pour éviter le lancement, comme https://stackoverflow.com/a/39209039/162530).

(Personnellement, j'aimerais aussi avoir l'option de compilateur (facultative) pour imposer que tout appelant à une fonction déclarée comme lancers doit également déclarer comme lancers ou les attraper.)

Mon cas d'utilisation actuel : je ne veux pas convertir l'intégralité de ma base de code [Angular] pour utiliser des exceptions pour la gestion des erreurs (vu que je les déteste). J'utilise async/wait dans les détails d'implémentation de mes API, mais je convertis le lancer en Promises/Observables normaux lorsqu'une API revient. Ce serait bien que le compilateur vérifie que j'attrape les bonnes choses (ou que je les attrape du tout, idéalement).

@aleksey-bykov Ne changeons pas la nature de JavaScript, ajoutons-y simplement du typage. :)

l'ajout de la saisie le modifie déjà (coupe le code qui n'a pas de sens),
de la même manière que nous pouvons renforcer la gestion des exceptions

Le jeudi 26 juillet 2018 à 20 h 22, Joe Pea [email protected] a écrit :

@aleksey-bykov https://github.com/aleksey-bykov Ne changeons pas le
nature de JavaScript, ajoutons-y simplement du typage. :)


Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/Microsoft/TypeScript/issues/13219#issuecomment-408274156 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AA5PzfUNS5E093Z74WA4WCUaTyWRRZC3ks5uKl1FgaJpZM4LXwLC
.

Pour les promesses, si vous utilisez le chaînage then au lieu de async/wait et évitez d'utiliser throw , tout fonctionne plutôt bien en fait. Voici une esquisse de preuve de concept :

https://bit.ly/2NQZD8i - lien aire de jeux

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

Le catch de prédicat supprime les types de l'union, tout en retournant un reject() qui leur est ajouté. La seule chose qui ne fonctionne pas tout à fait est l'inférence des types de paramètres d'union (probablement un bogue)

Problèmes abordés dans l'exemple ci-dessus :

  • ce n'est pas limité aux erreurs, vous pouvez rejeter n'importe quoi (bien que probablement une mauvaise idée)
  • ce n'est pas limité à l'utilisation de instanceof, vous pouvez utiliser n'importe quel type de prédicat pour supprimer les erreurs de l'union

Le principal problème que je vois en utilisant ceci pour tout le code sont les signatures de rappel, pour le faire fonctionner de manière compatible, le "type de retour" par défaut serait "might-throw-any", et si vous voulez restreindre cela, vous diriez throws X ou throws never si ce n'est pas le cas, par exemple

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

La version sans la signature :

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

serait en fait par défaut à

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

pour garantir que tout le code actuel compile.

Contrairement à strictNullChecks , où les valeurs de retour nulles sont assez rares, je pense que les exceptions dans JS sont assez répandues. Les modéliser dans des fichiers .d.ts n'est peut-être pas trop mal (importez des types à partir de dépendances pour décrire vos erreurs), mais ce sera certainement un effort non négligeable et se traduira par d'énormes unions.

Je pense qu'un bon terrain d'entente serait de se concentrer sur les promesses et async/await , car une promesse est déjà un wrapper, et le code asynchrone est l'endroit où la gestion des erreurs se ramifie le plus dans les scénarios typiques. D'autres erreurs seraient "non cochées"

@spion-h4 est-ce que cela peut (déjà/sera) utiliser sur le tapuscrit ?

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

@bluelovers
non, le texte dactylographié ne prend actuellement pas en charge le mot-clé throws , c'est de cela qu'il s'agit.

Quelque chose à noter (pas que cela bloque nécessairement cette proposition) est que sans classes nominales, taper des erreurs intégrées n'est toujours pas particulièrement utile car elles sont toutes structurellement identiques.

par exemple:

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
Cette question n'a rien à voir avec la proposition.
Vous pouvez rencontrer exactement le même problème en faisant ceci:

class BaseClass {
    propA!: string;
}

class MyClass1 extends BaseClass { }

class MyClass2 extends BaseClass { }

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

C'est une limitation de la langue qui affecte de nombreux cas d'utilisation.

Il y a une excellente lecture de @ahejlsberg sur les exceptions typées : https://www.artima.com/intv/handcuffs.html

Je pense que TypeScript est bien placé pour éviter ces problèmes. TypeScript propose des solutions _pragmatiques_ aux problèmes du _monde réel_. D'après mon expérience, dans les grandes bases de code JavaScript et TypeScript, la gestion des erreurs est l'un des plus gros problèmes - voir quelles erreurs les fonctions _pourraient_ lancer et que je _pourrais_ vouloir gérer est incroyablement difficile. Cela n'est possible qu'en lisant et en écrivant une bonne documentation (nous savons tous à quel point nous sommes bons dans ce / s) ou en regardant l'implémentation, et puisque contrairement aux valeurs de retour, les exceptions se propagent automatiquement via les appels de fonction, il ne suffit pas de vérifiez la fonction directement appelée, mais pour les attraper tous, vous devrez également vérifier les appels de fonction imbriqués. _C'est un problème du monde réel_.

Les erreurs en JavaScript sont en fait des valeurs très utiles. De nombreuses API dans NodeJS lancent des objets d'erreur détaillés, qui ont des codes d'erreur bien définis et exposent des métadonnées utiles. Par exemple, les erreurs de child_process.execFile() ont des propriétés comme exitCode et stderr , les erreurs de fs.readFile() ont des codes d'erreur comme ENOENT (fichier introuvable ) ou EPERM (autorisations insuffisantes). Je connais de nombreuses bibliothèques qui le font aussi, par exemple le pilote de base de données pg vous donne suffisamment de métadonnées sur une erreur pour savoir quelle contrainte de colonne exacte a causé l'échec d'un INSERT .

Vous pouvez voir une quantité inquiétante de vérifications regex fragiles sur les messages d'erreur dans les bases de code parce que les gens ne savent pas que les erreurs ont des codes d'erreur appropriés et ce qu'ils sont.

Si nous pouvions définir dans les déclarations @types et lib.d.ts quelles erreurs ces fonctions peuvent générer, TypeScript aurait le pouvoir de nous aider avec la structure de l'erreur - quelles erreurs possibles il peut y avoir, ce que le les valeurs de code d'erreur sont, quelles sont leurs propriétés. Il ne s'agit _pas_ d'exceptions typées et évite donc complètement tous les problèmes liés aux exceptions typées. C'est une _solution pragmatique_ à un _problème du monde réel_ (au lieu de dire aux gens d'utiliser des valeurs de retour d'erreur monadiques à la place - la réalité de JavaScript semble différente, les fonctions génèrent des erreurs).

L'annotation peut être complètement facultative (ou rendue obligatoire avec un indicateur de compilateur). Si elle n'est pas spécifiée dans un fichier de déclaration, une fonction lance simplement any (ou unknown ).
Une fonction peut déclarer manuellement ne jamais lancer avec throws never .
Si l'implémentation est disponible, une fonction lève une union de tous les types d'exception des fonctions qu'elle appelle et ses propres instructions throw qui ne sont pas à l'intérieur d'un bloc try avec un catch Clause
Si l'un d'eux lance any , la fonction lance également any - ce n'est pas grave, à chaque limite de fonction, le développeur a la possibilité de la corriger via une annotation explicite.
Et dans de nombreux cas, où une seule fonction bien connue est appelée et enveloppée dans un try/catch (par exemple, lire un fichier et le gérer introuvable), TypeScript peut alors déduire le type dans la clause catch.

Nous n'avons pas besoin de sous-classement d'erreur pour cela ni instanceof - les types d'erreur peuvent être des interfaces, qui spécifient des codes d'erreur avec des littéraux de chaîne, et TypeScript peut discriminer l'union sur le code d'erreur ou utiliser des gardes de type.

Définition des types d'erreur

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

Outre les erreurs, est-il également possible de marquer d'autres types d'effets secondaires comme

  • Divergence (boucle infinie / non retour)
  • OI
  • Non-déterminisme ( Math.random )

Cela me rappelle Koka de MSR qui peut marquer des effets sur les types de retour.

La proposition:

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

J'adore l'idée de déclaration de type d'erreur ! Cela serait d'une grande aide pour les personnes qui en ont besoin et ne fera rien de mal pour les personnes qui ne l'aiment pas. J'ai maintenant le problème dans mon projet de nœud, qui pourrait être résolu plus rapidement avec cette fonctionnalité. Je dois détecter les erreurs et renvoyer le code http approprié - pour ce faire, je dois toujours vérifier tous les appels de fonctions pour découvrir les exceptions que je dois gérer - et ce n'est pas amusant -_-

PS. La syntaxe dans https://github.com/Microsoft/TypeScript/issues/13219#issuecomment -428696412 semble très bonne - &! et les types d'erreur d'emballage dans <> sont tout simplement incroyables.

Je suis également en faveur de cela. Maintenant, toutes les erreurs ne sont pas typées et c'est assez ennuyeux. Bien que j'espère que la plupart des throws peuvent être dérivés du code, nous devons l'écrire partout.

De cette façon, les gens peuvent même écrire des règles tslint qui obligent le développeur à détecter les erreurs peu conviviales sur les points de terminaison de repos, etc.

J'ai été surpris que cette fonctionnalité ne soit pas _déjà_ à l'intérieur de TypeScript. Une des premières choses que je suis allé déclarer. Ce serait bien si le compilateur l'applique ou non, tant que vous pouvez obtenir l'information qu'il y aura une erreur générée et quel type d'erreurs sera générée.

Même si nous n'obtenons pas le &! et obtenons juste le Throws<T> ce serait :+1 :

C'est exactement ce que j'ai en tête, ce serait tellement plus beau si TypeScript supportait ces clauses.

Une autre fonctionnalité possible serait de forcer (peut-être au niveau de la fonction ou au niveau du module) strictExceptionTracking - ce qui signifierait que vous devriez invoquer tout ce qui est déclaré comme throws X avec une expression try! invokeSomething() comme dans Rust et Swift.

Il compilera simplement en invokeSomething() mais la possibilité d'exceptions sera visible dans le code, ce qui facilitera la détection si vous laissez quelque chose de mutable dans un mauvais état transitoire (ou si vous laissez les ressources allouées non disposées)

@be5invis

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

Je donne juste mes deux cents ici, mais le &! me semble incroyablement moche.
Je pense que les gens qui découvrent la dactylographie seraient un peu déconcertés par ce symbole, c'est vraiment peu engageant.
Un simple throws est plus explicite, intuitif et simple, à mon avis.

Autre que cela, +1 pour les exceptions typées. 👍

Je voudrais ajouter mon +1 pour les exceptions tapées et vérifiées.
J'ai beaucoup utilisé les exceptions typées dans mon code. Je suis pleinement conscient des pièges de instanceof , je l'ai en fait utilisé à mon avantage en étant capable d'écrire des gestionnaires génériques pour les erreurs associées qui héritent d'une classe de base commune. La plupart des autres méthodes que j'ai rencontrées pour éviter l'héritage de la classe Error de base finissent par être (au moins) tout aussi complexes et problématiques de différentes manières.
Les exceptions vérifiées sont une amélioration qui, à mon avis, se prête à une meilleure analyse statique de la base de code. Dans une base de code suffisamment complexe, il peut être facile de manquer les exceptions levées par une fonction particulière.

Pour ceux qui recherchent la sécurité des erreurs au moment de la compilation dans le tapuscrit, vous pouvez utiliser ma bibliothèque ts-results .

@vultix pour moi malheureusement, la séparation n'est pas assez claire pour mettre cela dans un cadre d'équipe.

@vultix L'approche de votre bibliothèque a été discutée ci-dessus, et c'est exactement le contraire de ce que cette fonctionnalité est là pour résoudre.
Javascript a déjà le mécanisme de gestion des erreurs, cela s'appelle des exceptions, mais le tapuscrit n'a pas de moyen de le décrire.

@nitzantomer Je suis tout à fait d'accord que cette fonctionnalité est une nécessité pour le tapuscrit. Ma bibliothèque n'est rien de plus qu'un remplaçant temporaire jusqu'à ce que cette fonctionnalité soit ajoutée.

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

Je sais que je suis un peu en retard pour ce jeu à ce sujet, mais ce qui suit semble un peu plus de syntaxe Typescripty que de virgule.

Iterable<number> throws TypeError | RangeError

Mais je suis toujours bon avec la virgule. J'aimerais juste que nous ayons cela dans la langue.
Le principal est que je l'aimerais sur JSON.parse() parce que beaucoup de mes collègues semblent oublier que JSON.parse peut générer une erreur et cela économiserait beaucoup d'allers-retours avec demandes d'extraction.

@WORMSS
Je suis complètement d'accord.
Ma proposition comprenait la syntaxe que vous recommandez :

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

Pouvons-nous suivre le modèle de déclaration d'erreur d'un langage POO comme Java ? Le mot-clé "throws" est utilisé pour déclarer que nous devons "try..catch" lors de l'utilisation d'une fonction avec une erreur potentielle

@allicanseenow
La proposition concerne l'utilisation facultative de la clause throws.
Autrement dit, vous n'aurez pas à utiliser try/catch si vous utilisez une fonction qui lance.

@allicanseenow , vous voudrez peut-être lire mon article ci-dessus pour le contexte, y compris l'article lié : https://github.com/microsoft/TypeScript/issues/13219#issuecomment -416001890

Je pense que c'est l'une des choses manquantes dans le système de type tapuscrit.
Son caractère purement facultatif, ne modifie pas le code émis, facilite le travail avec les bibliothèques et les fonctions natives.

De plus, savoir quelles erreurs peuvent être générées pourrait permettre aux éditeurs de "compléter automatiquement" la clause catch avec des conditions if pour gérer toutes les erreurs.

Je veux dire, même une petite application mérite une gestion appropriée des erreurs - le pire maintenant est que les utilisateurs ne savent pas quand quelque chose peut mal tourner.

Pour moi, c'est la fonction manquante TOP maintenant.

@RyanCavanaugh , il y a eu beaucoup de commentaires depuis l'ajout de l'étiquette "En attente de plus de commentaires". Des membres de l'équipe dactylographiée peuvent-ils intervenir ?

@nitzantomer Je soutiens l'idée d'ajouter des lancers à TypeScript, mais un try/catch facultatif ne permettrait-il pas des déclarations de lancers potentiellement inexactes lorsqu'elles sont utilisées dans des fonctions imbriquées?

Pour qu'un développeur puisse croire que la clause throws d'une méthode est exacte, il devrait faire l'hypothèse dangereuse qu'à aucun moment de la hiérarchie des appels de cette méthode, une exception facultative n'a été ignorée et n'a été renvoyée dans la pile sans être signalée. Je pense que @ajxs y a peut-être fait allusion à la fin de son commentaire, mais je pense que ce serait un gros problème. Surtout avec la fragmentation de la plupart des bibliothèques npm.

@ConnorSinnott
J'espère que j'ai bien compris :

Le compilateur déduira les types lancés, par exemple :

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

Sera en fait function fn(): throws string { ... } .
Le compilateur générera également une erreur en cas de non-concordance entre les erreurs déclarées et les erreurs réelles :

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

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

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

Le compilateur devrait se plaindre que fn2 lance string | MyError et non string .

Avec tout cela à l'esprit, je ne vois pas en quoi cette hypothèse est plus dangereuse que l'hypothèse selon laquelle d'autres types déclarés auxquels un développeur fait confiance lorsqu'il utilise d'autres bibliothèques, frameworks, etc.
Je ne suis pas sûr d'avoir vraiment couvert toutes les options, et je serais heureux si vous pouviez proposer un scénario intéressant.

Et même avec ce problème, c'est à peu près la même chose qu'aujourd'hui :

// 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) { ... }
    }
}

Nous pouvons même faire en sorte que le tapuscrit nous protège de cela, mais cela nécessitera une syntaxe un peu différente de javascript :

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

Celui-ci sera compilé en :

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

Hey! Merci d'avoir répondu! En fait, le fait de déduire les lancers comme vous l'avez mentionné atténue le problème auquel je pensais. Mais le deuxième exemple que vous avez cité est intéressant.

Donné

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

Pour que je déclare explicitement MyVeryImportantError, je devrais également déclarer explicitement toutes les erreurs supplémentaires de la pile d'appels, ce qui pourrait être une poignée en fonction de la profondeur de l'application. Aussi, potentiellement fastidieux. Je ne voudrais certainement pas plonger dans toute la chaîne d'appels pour générer une liste d'erreurs potentielles qui pourraient survenir en cours de route, mais peut-être que l'IDE pourrait aider.

Je pensais proposer une sorte d'opérateur de propagation pour permettre au développeur de déclarer explicitement son erreur et de simplement jeter le reste.

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

Mais il y aurait un moyen plus simple d'obtenir le même résultat : supprimez simplement la déclaration throws.

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

Ce qui soulève la question : quand est-ce que je verrais un avantage à utiliser le mot-clé throws plutôt que de simplement laisser le tapuscrit déduire les erreurs ?

@ConnorSinnott

Ce n'est pas différent des autres langages qui utilisent la clause throw, c'est-à-dire java.
Mais je pense que dans la plupart des cas, vous n'aurez pas vraiment à faire face à beaucoup de types. Dans le monde réel, vous ne traiterez généralement que string , Error (et leurs sous-classes) et différents objets ( {} ).
Et dans la plupart des cas, vous pourrez simplement utiliser une classe parent qui capture plus d'un type :

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 ce qui concerne l'utilisation implicite de la clause throw par rapport au fait que le compilateur l'infère, je pense que c'est comme déclarer des types dans un tapuscrit, dans de nombreux cas, vous n'êtes pas obligé de le faire, mais vous pouvez le faire pour mieux documenter votre code afin que celui qui le lit plus tard pourra mieux le comprendre.

Une autre approche si vous voulez la sécurité de type est de simplement traiter les erreurs comme des valeurs à la Golang :
https://gist.github.com/brandonkal/06c4a9c630369979c6038fa363ec6c83
Pourtant, ce serait une fonctionnalité intéressante à avoir.

@brandonkal
Cette approche a déjà été discutée plus tôt dans le fil.
C'est possible, mais cela revient à ignorer une partie/fonctionnalité inhérente que javascript nous permet.

Je pense aussi que le spécificateur noexcept (#36075) est la solution la plus simple et la meilleure en ce moment, comme pour la plupart des programmeurs, lancer des exceptions considère un anti-modèle.

Voici quelques fonctionnalités intéressantes :

  1. La vérification des fonctions ne génère pas d'erreur :
noexcept function foo(): void {
  throw new Error('Some exception'); // <- compile error!
}
  1. Avertissement concernant les blocs try..catch redondants :
try { // <- warning!
  foo();
} catch (e) {}
  1. Résolvez toujours les promesses :
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

comme pour la plupart des programmeurs, la levée d'exceptions est considérée comme un anti-modèle.

Je suppose que je ne fais pas partie de ce groupe "la plupart des programmeurs".
Le noexcept est cool et tout, mais ce n'est pas le sujet de ce problème.

Si les outils sont là, je les utiliserai. throw , try/catch et reject sont là, donc je vais les utiliser.

Ce serait juste bien de les faire taper correctement en tapuscrit.

Si les outils sont là, je les utiliserai. throw , try/catch et reject sont là, donc je vais les utiliser.

Sûr. Je vais les utiliser également. Ce que je veux dire, c'est que noexcept ou throws never sera un bon début pour ce problème.

Exemple stupide : je veux savoir que si j'appelle Math.sqrt(-2) , cela ne générera jamais d'erreur.

Il en va de même pour les bibliothèques tierces. Cela donne plus de contrôle sur le code et les cas extrêmes que je dois gérer en tant que programmeur.

@moshest
Mais pourquoi?
Cette proposition comprend tous les avantages de la proposition "noexpect", alors pourquoi se contenter de moins ?

Parce que cela prend du temps (ce problème a 3 ans), et j'espérais que nous pourrions au moins commencer par la fonctionnalité la plus basique.

@moshest
Eh bien, je préfère une meilleure fonctionnalité qui prend plus de temps qu'une solution partielle qui prendra moins de temps mais nous resterons coincés avec elle pour toujours.

J'ai l'impression que noexcept peut être facilement ajouté plus tard, en fait, cela pourrait être un sujet distinct.

noexcept est identique à throws never .

TBH Je pense qu'il est plus important d'avoir un mécanisme garantissant la gestion des exceptions (ala pas utilisé error dans Go) plutôt que de fournir des indications de type pour try/catch

@roll il ne devrait y avoir aucune "garantie".
ce n'est pas un must pour gérer les exceptions en javascript et cela ne devrait pas non plus être un must en tapuscrit.

Cela pourrait être une option dans le cadre du mode strict de typescrypt selon laquelle une fonction doit soit intercepter l'erreur, soit la déclarer explicitement throws et la transmettre

Pour ajouter mes 5 cents : je vois des exceptions similaires à une fonction renvoyant null : c'est une chose à laquelle vous ne vous attendez normalement pas. Mais si cela se produit, une erreur d'exécution se produit, ce qui est mauvais. Maintenant, TS a ajouté des "types non nullables" pour vous rappeler de gérer null . Je considère que l'ajout throws pousse ces efforts un peu plus loin, en vous rappelant de gérer également les exceptions. C'est pourquoi je pense que cette fonctionnalité est absolument nécessaire.

Si vous regardez Go, qui renvoie des erreurs au lieu de les lancer, vous pouvez voir encore plus clairement que les deux concepts ne sont pas si différents. En outre, cela vous aide à comprendre certaines API plus en profondeur. Peut-être que vous ne savez pas que certaines fonctions peuvent lancer et que vous le remarquez beaucoup plus tard dans la production (par exemple JSON.parse , qui sait combien il y en a de plus ?).

@ obedm503 C'est en fait une philosophie de TS : par défaut, le compilateur ne se plaint de rien. Vous pouvez activer des options pour traiter certaines choses comme une erreur ou activer le mode strict pour activer toutes les options à la fois. Donc, cela devrait être une donnée.

J'adore cette fonctionnalité suggérée.
Le typage et l'inférence des exceptions peuvent être l'une des meilleures choses de toute l'histoire de la programmation.
❤️

Bonjour, je viens de passer un peu de temps à essayer de trouver une solution de contournement avec ce que nous avons actuellement dans TS. Comme il n'y a aucun moyen d'obtenir le type des erreurs générées dans une portée de fonction (ni d'obtenir le type de la méthode actuelle en passant), j'ai compris comment nous pourrions cependant définir explicitement les erreurs attendues dans la méthode elle-même. Nous pourrions plus tard, récupérer ces types et au moins savoir ce qui pourrait être lancé dans la méthode.

Voici mon 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)

Merci beaucoup pour vos commentaires positifs! Je me rends compte qu'il pourrait être plus facile de refactoriser le code existant sans avoir à envelopper l'intégralité du type renvoyé dans le type throwable . Au lieu de cela, nous pourrions simplement ajouter celui-ci au type renvoyé, le code suivant vous permet d'ajouter les erreurs pouvant être lancées comme suit :

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

C'est le seul changement pour la partie exemple, voici la nouvelle déclaration de types :

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

J'ai également ajouté un nouveau type exceptionsOf qui permet d'extraire les erreurs d'une fonction afin d'escalader la responsabilité. Par exemple:

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

Comme exceptionsOf obtient une union d'erreurs, vous pouvez escalader autant de méthodes critiques que vous le souhaitez :

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

Je n'aime pas l'utilisation de typeof , si je trouve un meilleur moyen, je vous le ferai savoir

Vous pouvez tester ici les conseils de résultat : survolez typedError à la ligne 106

@Xample C'est une excellente solution avec les outils dont nous disposons maintenant !
Mais je pense qu'en pratique ce n'est pas suffisant puisque vous pouvez toujours faire des choses comme :

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

Et le type de b est déduit comme un nombre qui est correct mais juste s'il est enveloppé dans un essai
Cela ne devrait pas être valide

@luisgurmendezMLabs Je ne comprends pas très bien ce que vous voulez dire. Si vous suivez le dépôt de @Xample à la ligne 56. Vous pouvez voir que le résultat de myNumber + 23 est déduit comme number , tandis que myNumber: number & extraType<Error | CustomError> .

Dans une autre phase, a dans votre exemple n'est jamais enveloppé dans quelque chose comme un Try Monad. Il n'a pas du tout de wrapper autre qu'une intersection avec & extraType<Error | CustomError> .

Félicitations pour le superbe design @Xample 👏👏👏👏👏👏. C'est vraiment prometteur, et déjà utile même sans sucre syntaxique. Avez-vous l'intention de créer une bibliothèque de types pour cela ?

@ivawzh Je pense qu'en pratique, cela pourrait poser des problèmes car :

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

Ce retour de type de fonction est déduit comme un nombre et ce n'est pas tout à fait correct

@luisgurmendezMLabs le type jetable est dans le type de retour de la fonction uniquement comme moyen de coller le type d'erreur avec la fonction (et de pouvoir récupérer ces types plus tard). Je ne renvoie jamais les erreurs pour de vrai, je ne fais que les jeter. Par conséquent, si vous êtes dans un bloc try ou non, cela ne changera rien.

@ivawzh merci d'avoir demandé, je viens de le faire pour vous

@luisgurmendezMLabs ah ok je comprends votre point, il semble que le tapuscrit ne déduise que le premier type trouvé. Par exemple si vous aviez :

function stillARiskyMethod() { 
    return superRiskyMethod();
}

le type de retour de stillARiskyMethod serait déduit correctement, tandis que

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

taperait seulement le type de retour comme superRiskyMethod() et rejetterait celui de anotherSuperRiskyMethod() . je n'ai pas enquêté plus

Pour cette raison, vous devez escalader manuellement le type d'erreur.

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

Je voulais aussi juste laisser tomber mes pensées à ce sujet.

Je pense que ce serait une très bonne fonctionnalité à implémenter car il existe différents cas d'utilisation pour vouloir renvoyer une erreur/null et vouloir lancer quelque chose et cela pourrait avoir beaucoup de potentiel. Lancer des exceptions fait de toute façon partie du langage Javascript, alors pourquoi ne devrions-nous pas donner la possibilité de les saisir et de les déduire ?

Par exemple, si je fais beaucoup de tâches qui pourraient provoquer des erreurs, je trouverais peu pratique d'avoir à utiliser une instruction if pour vérifier le type de retour à chaque fois, alors que cela pourrait être simplifié en utilisant un try/catch où le cas échéant l'une de ces tâches est lancée, elle sera gérée dans le catch sans aucun code supplémentaire.

Ceci est particulièrement utile lorsque vous allez gérer les erreurs de la même manière ; par exemple dans express/node.js, je souhaiterais peut-être transmettre l'erreur au NextFunction (gestionnaire d'erreurs).

Plutôt que de faire if (result instanceof Error) { next(result); } chaque fois, je pourrais simplement envelopper tout le code de ces tâches dans un try/catch, et dans mon catch, je sais qu'une exception a été levée, je voudrai toujours le transmettre à mon gestionnaire d'erreurs, donc peut catch(error) { next(error); }

Je n'ai pas encore vu cela discuté (peut-être l'avez-vous manqué cependant, ce fil a pas mal de commentaires!) Mais si cela était implémenté, serait-il obligatoire (c'est-à-dire: erreur de compilation) d'avoir une fonction qui lance sans utiliser le Clause throws dans sa déclaration de fonction ? J'ai l'impression que ce serait bien de le faire (nous ne forçons pas les gens à gérer les lancers, cela les informerait simplement que la fonction lance) mais la grande préoccupation ici est que si Typescript était mis à jour de cette manière, il serait probablement casser beaucoup de code existant actuellement.

Edit: Un autre cas d'utilisation auquel j'ai pensé pourrait être que cela aiderait également à la génération de JSDocs à l'aide de la balise @throws

Je vais répéter ici ce que j'ai dit dans le numéro maintenant engagé .


Je pense que le tapuscrit devrait pouvoir déduire le type d'erreur de la plupart des expressions JavaScript. Permettre une mise en œuvre plus rapide par les créateurs de bibliothèques.

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();
}
  • Fonction a nous savons que le type d'erreur est 'unlucky' , et si nous voulons être très prudents, nous pouvons l'étendre à Error | 'unlucky' , d'où includeError .
  • La fonction b héritera du type d'erreur de la fonction a .
  • La fonction c est presque identique ; 'unlucky' | 'fairly lucky' , ou Error | 'unlucky' | 'fairly lucky' .
  • La fonction d devra lancer unknown , car eval est... inconnu
  • La fonction e attrape l'erreur de d , mais puisqu'il y a throw dans le bloc catch, nous en déduisons son type 'too bad...' , ici, puisque le bloc uniquement contient throw 'primitive value' on pourrait dire qu'il ne peut pas jeter Error (Corrigez-moi si j'ai raté de la magie noire JS ...)
  • La fonction f hérite de c comme b l'a fait de a .
  • La fonction g hérite 'unlucky' de a et unknown de c donc 'unlucky' | unknown => unknown

Voici quelques options de compilateur qui, selon moi, devraient être incluses, car l'engagement des utilisateurs avec cette fonctionnalité peut varier en fonction de leurs compétences ainsi que de la sécurité de type des bibliothèques dont ils dépendent dans un projet donné :

{
  "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
    }
  }
}

Quant à la syntaxe sur la façon d'exprimer l'erreur que toute fonction peut produire, je ne suis pas sûr, mais je sais que nous avons besoin de la capacité pour qu'elle soit générique et déductible.

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;

Mon cas d'utilisation, car montrer un cas d'utilisation réel peut montrer qu'il a une valeur :

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

J'ai besoin d'un puits d'erreur pour empêcher l'envoi d'en-têtes de réponse plusieurs fois pour plusieurs erreurs (elles ne se produisent généralement pas, mais quand elles le font, elles le font en masse !)

Ce problème est toujours marqué Awaiting More Feedback , que pourrions-nous faire pour fournir plus de commentaires ? C'est la seule fonctionnalité que j'envie du langage java

Nous avons un cas d'utilisation où nous lançons une erreur spécifique lorsqu'un appel d'API renvoie du code non-200 :

interface HttpError extends Error {
  response: Response
}

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

Ne pas pouvoir taper le bloc catch fait que les développeurs oublient que 2 types d'erreurs possibles peuvent être générés et qu'ils doivent gérer les deux.

Je préfère toujours lancer l'objet 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')) { .....}
  }
}

Pourquoi cette fonctionnalité n'a-t-elle toujours "pas assez de retours" ? C'est tellement utile lors de l'appel de l'API du navigateur comme indexedDB, localstorage. Cela a causé de nombreux échecs dans des scénarios complexes, mais le développeur ne peut pas en être conscient lors de la programmation.

Hegel semble avoir parfaitement cette caractéristique.
https://hegel.js.org/docs#benefits (faites défiler jusqu'à la section "Erreur typée")

Je souhaite que TypeScript ait une fonctionnalité similaire!

DL;DR;

  • La fonction de rejet des promesses doit être typée
  • Tout type lancé dans un bloc try doit être déduit à l'aide d'une union dans l'argument d'erreur du catch
  • error.constructor doit être correctement typé en utilisant le type réel et pas seulement any pour éviter de manquer une erreur générée.

D'accord, peut-être devrions-nous simplement clarifier nos besoins et nos attentes :

Les erreurs sont généralement gérées de 3 manières dans js

1. La voie du nœud

Utilisation de rappels (qui peuvent en fait être tapés)

Exemple d'utilisation :

import * as fs from 'fs';

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

fs.d.ts nous donne :

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

Par conséquent, l'erreur est tapée comme suit

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

2. La voie de la promesse

Lorsque la promesse est résolue ou rejetée, alors que vous pouvez taper le résolu value , vous ne pouvez pas taper le rejet souvent appelé reason .

Voici la signature du constructeur d'une promesse : Notez que la raison est tapée any

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

Il aurait été théoriquement possible de les taper comme suit :

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

De cette manière, nous pourrions encore théoriquement faire une conversion 1-1 entre un rappel de nœud et une promesse, en gardant tout le typage tout au long de ce processus. Par exemple:

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 méthode "essayer et attraper"

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

Bien que nous lancions une erreur "Error" 2 lignes avant le bloc catch, TS est incapable de taper le error (valeur) comme Error (type).

N'est-ce pas le cas non plus en utilisant des promesses dans une fonction async (il n'y a pas encore de magie). En utilisant notre rappel de nœud promis :

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

Il n'y a pas d'informations manquantes pour que le tapuscrit puisse prédire quel type d'erreur pourrait être généré dans une portée d'essai.
Nous devons cependant considérer que les fonctions natives internes peuvent générer des erreurs qui ne se trouvent pas dans le code source, mais si chaque fois qu'un mot-clé "throw" est dans la source, TS doit rassembler le type et le suggérer comme type possible pour l'erreur. Ces types seraient bien sûr limités par le bloc try .

Ce n'est que la première étape et il y aura encore place à l'amélioration, comme un mode strict forçant TS à fonctionner comme en Java c'est-à-dire pour forcer l'utilisateur à utiliser une méthode risquée (une méthode qui peut lancer quelque chose) dans un try Bloc function example throw ErrorType { ... } pour escalader la responsabilité de la gestion des erreurs.

Dernier point mais non le moindre : évitez de manquer une erreur

Tout peut être lancé, pas seulement une erreur ou même une instance d'un objet. Ce qui signifie que ce qui suit est valide

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

Savoir que l'erreur pourrait être de type number | Error serait extrêmement utile. Cependant, pour éviter d'oublier de gérer un type d'erreur possible, ce n'est pas vraiment la meilleure idée d'utiliser des blocs if / else séparés sans un ensemble strict de résultats possibles.
Un cas de commutation ferait cependant beaucoup mieux car nous pouvons être avertis si nous oublions de faire correspondre un cas spécifique (lequel reviendrait à la clause par défaut). Nous ne pouvons pas (sauf si nous faisons quelque chose de hackish) changer de casse un type d'instance d'objet, et même si nous le pouvions, nous pouvons jeter n'importe quoi (pas seulement un objet mais aussi un booléen, une chaîne et un nombre qui n'ont pas "d'instance"). Cependant, nous pouvons utiliser le constructeur de l'instance pour savoir de quel type il s'agit. Nous pouvons maintenant réécrire le code ci-dessus comme suit :

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… le seul problème restant est que TS ne tape pas le error.constructor et donc il n'y a aucun moyen de réduire le cas du commutateur (encore ?), Si c'était le cas, nous aurions un langage d'erreur typé sûr pour js.

Veuillez commenter si vous avez besoin de plus de commentaires

Cette page vous a été utile?
0 / 5 - 0 notes