Typescript: Sugestão: cláusula `throws` e cláusula catch digitada

Criado em 29 dez. 2016  ·  135Comentários  ·  Fonte: microsoft/TypeScript

O sistema de tipos de texto datilografado é útil na maioria dos casos, mas não pode ser utilizado ao lidar com exceções.
Por exemplo:

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

O problema aqui é duplo (sem examinar o código):

  1. Ao usar esta função, não há como saber que ela pode gerar um erro
  2. Não está claro qual(is) tipo(s) de erro será(ão)

Em muitos cenários, isso não é realmente um problema, mas saber se uma função/método pode lançar uma exceção pode ser muito útil em diferentes cenários, especialmente ao usar bibliotecas diferentes.

Ao introduzir a exceção verificada (opcional), o sistema de tipos pode ser utilizado para tratamento de exceção.
Eu sei que as exceções verificadas não são acordadas (por exemplo, Anders Hejlsberg ), mas ao torná-lo opcional (e talvez inferido? mais tarde), ele apenas adiciona a oportunidade de adicionar mais informações sobre o código que podem ajudar desenvolvedores, ferramentas e documentação.
Também permitirá um melhor uso de erros personalizados significativos para grandes projetos grandes.

Como todos os erros de tempo de execução de javascript são do tipo Error (ou tipos de extensão como TypeError ), o tipo real de uma função sempre será type | Error .

A gramática é direta, uma definição de função pode terminar com uma cláusula throws seguida por um tipo:

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

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

Ao capturar as exceções, a sintaxe é a mesma com a capacidade de declarar o(s) tipo(s) do erro:
catch(e: string | Error) { ... }

Exemplos:

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

Aqui fica claro que a função pode lançar um erro e que o erro será uma string, então ao chamar este método o desenvolvedor (e o compilador/IDE) está ciente disso e pode lidar melhor com isso.
Assim:

fn(0);

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

Compila sem erros, mas:

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

Falha ao compilar porque number não é string .

Controle de fluxo e inferência de tipo de erro

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

Lança string .

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

    fn(num);
}

Lança MyError | string .
Contudo:

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

Lança apenas MyError .

Awaiting More Feedback Suggestion

Comentários muito úteis

@aleksey-bykov

Você está sugerindo não usar throw no meu código e, em vez disso, envolver os resultados (em funções que podem dar erro).
Esta abordagem tem algumas desvantagens:

  • Este encapsulamento cria mais código
  • Requer que toda a cadeia de função invocada retorne este valor encapsulado (ou erro) ou alternativamente a função que recebe Tried<> não pode optar por ignorar o erro.
  • Não é um padrão, bibliotecas de terceiros e o js nativo lança erros

Adicionar throws permitirá que os desenvolvedores que optem por lidar com erros de seu código, 3ª bibliotecas e js nativos.
Como a sugestão também solicita inferência de erros, todos os arquivos de definição gerados podem incluir a cláusula throws .
Será muito conveniente saber quais erros uma função pode lançar diretamente do arquivo de definição em vez do estado atual em que você precisa ir para os documentos, por exemplo, para saber qual erro JSON.parse pode lançar eu preciso ir para a página MDN e leia que:

Lança uma exceção SyntaxError se a string a ser analisada não for um JSON válido

E este é o bom caso quando o erro é documentado.

Todos 135 comentários

Apenas para esclarecer - uma das idéias aqui não é forçar os usuários a capturar a exceção, mas sim, inferir melhor o tipo de uma variável da cláusula catch?

@Daniel Rosenwasser
Sim, os usuários não serão forçados a capturar exceções, então tudo bem com o compilador (em tempo de execução, o erro é lançado, é claro):

function fn() {
    throw "error";
}

fn();

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

Mas dará aos desenvolvedores uma maneira de expressar quais exceções podem ser lançadas (seria incrível ter isso ao usar outros arquivos de bibliotecas .d.ts ) e então fazer com que o tipo de compilador proteja os tipos de exceção dentro da cláusula catch.

como um lance verificado é diferente 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);
}

em vez de

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

@aleksey-bykov

Você está sugerindo não usar throw no meu código e, em vez disso, envolver os resultados (em funções que podem dar erro).
Esta abordagem tem algumas desvantagens:

  • Este encapsulamento cria mais código
  • Requer que toda a cadeia de função invocada retorne este valor encapsulado (ou erro) ou alternativamente a função que recebe Tried<> não pode optar por ignorar o erro.
  • Não é um padrão, bibliotecas de terceiros e o js nativo lança erros

Adicionar throws permitirá que os desenvolvedores que optem por lidar com erros de seu código, 3ª bibliotecas e js nativos.
Como a sugestão também solicita inferência de erros, todos os arquivos de definição gerados podem incluir a cláusula throws .
Será muito conveniente saber quais erros uma função pode lançar diretamente do arquivo de definição em vez do estado atual em que você precisa ir para os documentos, por exemplo, para saber qual erro JSON.parse pode lançar eu preciso ir para a página MDN e leia que:

Lança uma exceção SyntaxError se a string a ser analisada não for um JSON válido

E este é o bom caso quando o erro é documentado.

E este é o bom caso quando o erro é documentado.

existe uma maneira confiável em javascript para distinguir SyntaxError de Error?

  • sim, é mais código, mas como uma situação ruim é representada em um objeto, ela pode ser repassada para ser processada, descartada, armazenada ou transformada em um resultado válido como qualquer outro valor

  • você pode ignorar try retornando try também, try pode ser visto como uma mônada, procure por cálculos monádicos

    function mightFail(): Tried<number, string> {
    }
    function mightFailToo(): Tried<number, string> {
        const tried = mightFail();
        if (isSuccess(tried))  { 
             return successFrom(tried.result * 2);
        } else {
             return tried;
        }
    }
    
  • é padrão o suficiente para o seu código, quando se trata de libs de terceiros lançando uma exceção, geralmente significa um gameover para você, porque é quase impossível recuperar de maneira confiável de uma exceção, o motivo é que ela pode ser lançada de qualquer lugar dentro do código terminando -o em uma posição arbitrária e deixando seu estado interno incompleto ou corrompido

  • não há suporte para exceções verificadas do tempo de execução do JavaScript e temo que não possa ser implementado apenas no script datilografado

além de codificar uma exceção como um caso de resultado especial é uma prática muito comum no mundo FP

considerando que dividir um resultado possível em 2 partes:

  • um entregue pela declaração de retorno e
  • outro entregue por lance

parece uma dificuldade inventada

na minha opinião, throw é bom para falhar rápido e alto quando nada você pode fazer sobre isso, resultados explicitamente codificados são bons para qualquer coisa que implique uma situação ruim, mas esperada, da qual você pode se recuperar

considerar:

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

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

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

@aleksey-bykov

Meu ponto com JSON.parse pode lançar SyntaxError é que eu preciso procurar a função nos documentos apenas para saber que ela pode lançar, e seria mais fácil ver isso no .d.ts .
E sim, você pode saber que é SyntaxError usando instanceof .

Você pode representar a mesma situação ruim lançando um erro.
Você pode criar sua própria classe de erro que estende Error e colocar todos os dados relevantes que você precisa nela.
Você está obtendo o mesmo com menos código.

Às vezes, você tem uma longa cadeia de invocações de função e pode querer lidar com alguns dos erros em diferentes níveis da cadeia.
Será muito chato usar sempre resultados agrupados (mônadas).
Sem mencionar que, novamente, outras bibliotecas e erros nativos podem ser lançados de qualquer maneira, então você pode acabar usando tanto as mônadas quanto o try/catch.

Eu discordo de você, em muitos casos você pode se recuperar de erros lançados, e se a linguagem permitir que você expresse melhor, será mais fácil fazê-lo.

Como em muitas coisas em typescript, a falta de suporte do recurso em javascript não é um problema.
Isto:

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

Funcionará como esperado em javascript, apenas sem a anotação de tipo.

Usar throw é suficiente para expressar o que você está dizendo: se a operação foi bem-sucedida, retorne o valor, caso contrário, lance um erro.
O usuário desta função decidirá então se deseja lidar com os possíveis erros ou ignorá-los.
Você pode lidar apenas com erros que você mesmo lançou e ignorar os que são de terceiros, por exemplo.

se estamos falando de navegadores instanceof só é bom para coisas que se originam da mesma janela/documento, tente:

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

então quando você faz:

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

você não vai pegar

outro problema com exceções que podem entrar em seu código de muito além de onde você espera que aconteçam

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

além de instanceof ser vulnerável à herança de protótipo, então você precisa ter cuidado extra para sempre verificar o ancestral 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 Explicitamente encadear erros como você sugere em estruturas monádicas é uma tarefa bastante difícil e assustadora. É preciso muito esforço, torna o código difícil de entender e requer suporte de linguagem / emissão orientada por tipo para estar à beira de ser suportável. Este é um comentário vindo de alguém que se esforça muito para popularizar Haskell e FP como um todo.

É uma alternativa de trabalho, principalmente para entusiastas (inclusive eu), porém não acho que seja uma opção viável para o público maior.

Na verdade, minha principal preocupação aqui é que as pessoas comecem a subclassificar Error. Eu acho que esse é um padrão terrível. De maneira mais geral, qualquer coisa que promova o uso do operador instanceof só vai criar confusão adicional em torno das classes.

Este é um comentário vindo de alguém que se esforça muito para popularizar Haskell e FP como um todo.

eu realmente acho que isso deve ser empurrado com mais força para o público, não até que seja digerido e solicitado por mais, podemos ter um melhor suporte de FP no idioma

e não é tão assustador quanto você pensa, desde que todos os combinadores já estejam escritos, basta usá-los para construir um fluxo de dados, como fazemos em nosso projeto, mas concordo que o TS poderia ter suportado melhor: #2319

Os transformadores Monad são um verdadeiro PITA. Você precisa de levantamento, içamento e corrida seletiva com bastante frequência. O resultado final é um código dificilmente compreensível e uma barreira de entrada muito maior do que o necessário. Todos os combinadores e funções de levantamento (que fornecem o boxing/unboxing obrigatório) são apenas ruídos que o desviam do problema em questão. Eu acredito que ser explícito sobre estado, efeitos, etc é uma coisa boa, mas acho que ainda não encontramos um encapsulamento / abstração conveniente. Até encontrá-lo, apoiar padrões de programação tradicionais parece ser o caminho a percorrer sem parar para experimentar e explorar nesse meio tempo.

PS: Acho que precisamos de mais do que operadores personalizados. Tipos de Tipos Superiores e algum tipo de classe de tipos também são essenciais para uma biblioteca monádica prática. Entre eles, eu classificaria o HKT primeiro e digitaria classes em segundo lugar. Com tudo isso dito, acredito que TypeScript não é a linguagem para praticar tais conceitos. Brincando - sim, mas sua filosofia e raízes estão fundamentalmente distantes para uma integração perfeita e adequada.

De volta à pergunta do OP - instanceof é um operador perigoso de usar. No entanto, exceções explícitas não estão limitadas a Error . Você também pode lançar seus próprios ADTs ou erros POJO personalizados. O recurso proposto pode ser bastante útil e, claro, também pode ser mal utilizado. De qualquer forma, torna as funções mais transparentes, o que sem dúvida é uma coisa boa. No geral, estou 50/50 nisso :)

@aleksey-bykov

Os desenvolvedores devem estar cientes dos diferentes problemas de js que você descreveu, afinal adicionar throws ao typescript não introduz nada de novo ao js, ​​apenas dá ao typescript como linguagem a capacidade de expressar um comportamento js existente.

O fato de que bibliotecas de terceiros podem lançar erros é exatamente o que quero dizer.
Se seus arquivos de definição incluíssem isso, terei uma maneira de saber.

@aluanhaddad
Por que é um padrão terrível estender Error ?

@gcnew
Quanto a instanceof , isso foi apenas um exemplo, eu sempre posso lançar objetos regulares que têm tipos diferentes e depois usar guardas de tipo para diferenciá-los.
Caberá ao desenvolvedor decidir que tipo de erros ele deseja lançar, e provavelmente já é o caso, mas atualmente não há como expressar isso, que é o que esta sugestão quer resolver.

@nitzantomer A subclassificação de classes nativas ( Error , Array , RegExp , etc) não era suportada em versões ECMAScript mais antigas (antes do ES6). A emissão de nível inferior para essas classes fornece resultados inesperados (o melhor esforço é feito, mas isso é o máximo que se pode ir) e é o motivo de vários problemas registrados diariamente. Como regra geral - não subclassifique nativos a menos que você esteja direcionando versões recentes do ECMAScript e realmente saiba o que está fazendo.

@gcnew
Oh, estou bem ciente disso, pois passei mais de algumas horas tentando descobrir o que deu errado.
Mas com a capacidade de fazê-lo agora, não deve haver motivo para não (ao direcionar es6).

De qualquer forma, essa sugestão não pressupõe que o usuário esteja subclassificando a classe Error, foi apenas um exemplo.

@nitzantomer Não estou argumentando que a sugestão é limitada a Error . Acabei de explicar por que é um padrão ruim subclassificá-lo. No meu post eu defendi a posição de que objetos personalizados ou sindicatos discriminados também podem ser usados.

instanceof é perigoso e considerado um antipadrão mesmo se você eliminar as especificidades do JavaScript - por exemplo , cuidado com o operador instanceof . A razão é que o compilador não pode protegê-lo contra bugs introduzidos por novas subclasses. A lógica usando instanceof é frágil e não segue o princípio aberto/fechado , pois espera apenas um punhado de opções. Mesmo se um curinga for adicionado, novas derivadas ainda podem causar erros, pois podem quebrar suposições feitas no momento da escrita.

Para os casos em que você deseja distinguir entre alternativas conhecidas, o TypeScript possui Tagged Unions (também chamadas de uniões discriminadas ou tipos de dados algébricos). O compilador garante que todos os casos sejam tratados, o que lhe dá boas garantias. A desvantagem é que, se você quiser adicionar uma nova entrada ao tipo, terá que passar por todo o código discriminando-o e lidar com a opção recém-adicionada. A vantagem é que esse código provavelmente teria sido quebrado, mas teria falhado em tempo de execução.

Eu apenas pensei duas vezes nessa proposta e me tornei contra ela. A razão é que se as declarações throws estavam presentes nas assinaturas, mas não foram impostas, elas já podem ser tratadas pelos comentários da documentação. No caso de serem aplicadas, compartilho o sentimento de que elas se tornariam irritantes e engolidas rapidamente, pois o JavaScript não possui o mecanismo do Java para cláusulas catch tipadas. Usar exceções (especialmente como fluxo de controle) também nunca foi uma prática estabelecida. Tudo isso me leva ao entendimento de que exceções verificadas trazem muito pouco, enquanto formas melhores e atualmente mais comuns de representar falhas estão disponíveis (por exemplo, retorno sindical).

@gcnew
É assim que é feito em C#, o problema é que os documentos não são padrão no typescript.
Não me lembro de encontrar um arquivo de definição que esteja bem documentado. Os diferentes arquivos lib.d.ts contêm comentários, mas não contêm erros lançados (com uma exceção: lib.es6.d.ts tem um throws em Date[Symbol.toPrimitive](hint: string) ).

Além disso, essa sugestão leva em consideração a inferência de erros, algo que não acontecerá se os erros forem provenientes de comentários de documentação. Com exceções verificadas inferidas, o desenvolvedor nem precisará especificar a cláusula throws , o compilador a inferirá automaticamente e a usará para compilação e a adicionará ao arquivo de definição resultante.

Concordo que impor o tratamento de erros não é uma coisa boa, mas ter esse recurso apenas adicionará mais informações que poderão ser usadas por quem desejar.
O problema com:

... existem maneiras melhores e atualmente mais comuns de representar o fracasso

É que não há uma maneira padrão de fazer isso.
Você pode usar o retorno de união, @aleksey-bykov usará Tried<> e um desenvolvedor de outra biblioteca de terceiros fará algo completamente diferente.
Lançar erros é um padrão em todas as linguagens (js, java, c#...) e como faz parte do sistema e não uma solução, deveria (na minha opinião) ter um melhor manuseio em typescript, e uma prova disso é o número de problemas que vi aqui ao longo do tempo que pedem anotação de tipo na cláusula catch .

Eu adoraria ter informações na dica de ferramenta no VS se uma função (ou função chamada) pode ser lançada. Para arquivos *.d.ts , provavelmente precisamos de um parâmetro falso como este desde o TS2.0.

@HolgerJeromin
Por que seria necessário?

aqui está uma pergunta simples, qual assinatura deve ser inferida para dontCare no código abaixo?

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

function dontCare() {
   return mightThrow();
}

de acordo com o que você disse em sua proposta, deve ser

function dontCare(): void throws string {

eu digo que deve ser um erro de tipo, pois uma exceção verificada não foi tratada corretamente

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

por que é que?

porque, caso contrário, há uma boa chance de obter o estado do chamador imediato corrompido:

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

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

se você deixar escapar uma exceção, você não pode inferir como marcada, porque o contrato de comportamento de keepAllValues seria violado dessa maneira (nem todos os valores foram mantidos, apesar da intenção original)

a única maneira segura é pegá-los imediatamente e relançá-los explicitamente

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

caso contrário, apesar de os chamadores saberem o que pode ser feito, você não pode dar a eles garantias de que é seguro continuar usando o código que acabou de lançar

portanto , não existe propagação automática de contrato de exceção verificada

e corrija-me se estiver errado, é exatamente isso que o Java faz, que você mencionou como exemplo anteriormente

@aleksey-bykov
Isto:

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

function dontCare() {
   return mightThrow();
}

Significa que mightThrow e dontCare são inferidos para throws string , no entanto:

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

Não terá uma cláusula throw porque o erro foi tratado.
Isto:

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

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

Terá throws MyErrorType .

Quanto ao seu exemplo keepAllValues , não tenho certeza do que você quer dizer, no seu exemplo:

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

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

MyClass.keepAllValues será inferido como throws string porque mightThrow pode gerar um string e esse erro não foi tratado.

Quanto ao seu exemplo keepAllValues , não tenho certeza do que você quer dizer

Eu quis dizer que as exceções não tratadas de mightThrow interrompem keepAllValues e fazem com que ele termine no meio do que estava fazendo, deixando seu estado corrompido. Isso é um problema. O que você sugere é fechar os olhos para esse problema e fingir que não é sério. O que eu sugiro é resolver esse problema exigindo que todas as exceções verificadas sejam tratadas imediatamente e explicitamente relançadas. Dessa forma, não há como corromper o estado involuntariamente . E embora ainda possa estar corrompido se você assim o desejar, isso exigiria alguma codificação deliberada.

Pense nisso, existem 2 maneiras pelas quais podemos lidar com exceções:

  • desmanche-os, o que leva a uma falha, se a falha é o que você deseja, estamos bem aqui
  • se você não quer uma falha, então você precisa de alguma orientação sobre que tipo de exceção você precisa procurar, e é aí que sua proposta entra: exceções marcadas - todas explicitamente listadas, para que você possa lidar com todas elas e não não perca nada

agora, se decidimos ir com as exceções verificadas que são tratadas corretamente e evitar uma falha, precisamos descartar uma situação em que lidamos com uma exceção vinda de várias camadas profundas de onde você está capturando:

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

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

o exemplo acima traz um caso interessante para consideração, qual seria a assinatura inferida de:

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

outro caso interessante

// 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
Então você propõe que todos os erros devem ser tratados como é com java?
Eu não sou fã disso (mesmo vindo de java e ainda amando) porque js/ts são muito mais dinâmicos e seus usuários estão acostumados a isso.
Pode ser um sinalizador que faz você lidar com erros se você o incluir ao compilar (como strictNullChecks ).

Minha sugestão não está aqui para resolver exceções não tratadas, o código que você postou quebrará agora sem esse recurso implementado e também quebraria em js.
Minha sugestão apenas permite que você, como desenvolvedor, esteja mais ciente dos diferentes erros que podem ser lançados, ainda depende de você lidar com eles ou ignorá-los.

Quanto ao problema da divisão por 0, não resulta em erro:

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

mais conscientes dos diferentes erros que podem ser lançados

não adianta fazer isso a menos que eles possam lidar com eles, a proposta atual não é viável por causa dos casos que listei

Então você propõe que todos os erros devem ser tratados como é com java?

sim, é isso que significa ter verificado as exceções

@aleksey-bykov
Não vejo por que algum dos casos que você listou torna essa proposta inviável.

Não há problema em lidar com um erro que foi lançado na cadeia de invocação, mesmo se eu estiver usando uma função que foi inferida de lançar DivisionByZero (independentemente de onde foi lançado), posso optar por lidar com isso .
Posso tentar novamente com argumentos diferentes, posso mostrar ao usuário uma mensagem de que algo deu errado, posso registrar esse problema para depois alterar meu código para lidar com isso (se acontecer com frequência).

Novamente, esta proposta não altera nada em tempo de execução, então tudo que funcionou continuará funcionando como antes.
A única diferença é que terei mais informações sobre os erros que podem ser lançados.

eu vejo o que você está dizendo, nada será alterado no tempo de execução do javascript, no entanto, sua mensagem aqui é dar aos usuários alguma ilusão de que eles sabem o que estão fazendo, manipulando uma exceção que veio de 20 camadas abaixo com a mesma confiança que eles lidariam com uma exceção imediata

simplesmente não há como eles resolverem um problema que aconteceu 20 camadas abaixo

você pode registrá-lo, claro, assim como qualquer exceção não verificada, mas não pode corrigi-lo

então é uma mentira de um modo geral, há mentiras suficientes no TS, não vamos confundir as pessoas ainda mais

@aleksey-bykov
O que você está descrevendo existe em todos os idiomas que oferecem suporte a exceções.
Ninguém disse que a captura de uma exceção resolveria o problema, mas permitirá que você lide com isso de maneira graciosa.

Saber quais erros podem ser lançados ao invocar uma função ajudará os desenvolvedores a separar os erros com os quais eles podem lidar e os que não podem.

No momento, os desenvolvedores podem não saber que usar JSON.parse pode gerar um erro, mas se fosse parte do lib.d.ts e o IDE o informasse (por exemplo), talvez ele optasse por lidar com este caso.

você não pode lidar com um problema que aconteceu 20 camadas abaixo normalmente, porque o estado interno está corrompido em 19 camadas e você não pode ir lá porque o estado é privado

para ser construtivo: o que estou sugerindo é exigir que os usuários manipulem as exceções verificadas imediatamente e as relancem explicitamente, desta forma, descartamos a confusão não intencional e separamos as exceções verificadas das não verificadas:

  • exceção verificada: aconteceu no alcance imediato e deve ser tratada, desta forma é garantido que o estado não está corrompido e é seguro prosseguir
  • exceção não verificada: aconteceu no alcance imediato ou muito mais abaixo, não pode ser tratado porque o estado estava corrompido, você pode registrá-lo ou prosseguir por sua conta e risco

SyntaxError em JSON.parse deve ser declarado como uma exceção verificada

@aleksey-bykov

Não vejo por que há necessidade de obrigar os desenvolvedores a fazer algo que eles não desejam, algo que eles não fizeram até agora.

Aqui está um exemplo:
Eu tenho um cliente da web no qual o usuário pode gravar/colar dados json e clicar em um botão.
O aplicativo pega essa entrada e a passa para uma biblioteca de terceiros que de alguma forma analisa esse json e retorna o json junto com os diferentes tipos de valores (string, number, boolean, array, etc).
Se esta biblioteca de terceiros lançar um SyntaxError eu posso recuperar: informe ao usuário que sua entrada é inválida e ele deve tentar novamente.

Ao saber quais erros podem ser lançados ao invocar uma função, o desenvolvedor pode decidir o que ele pode/deseja manipular e o que não.
Não importa o quão profundo na cadeia o erro foi lançado.

olha você não parece entender o que estou dizendo, estamos andando em círculos

ao permitir que SyntaxError seja lançado da biblioteca de terceiros, você está expondo seu usuário aos detalhes de implementação de seu próprio código que devem ser encapsulados

basicamente você está dizendo, ei, não é meu código que não funciona, é aquela biblioteca estúpida que encontrei na internet e usei, então se você tiver um problema com isso, lide com essa lib de terceiros, não eu, eu acabei de dizer o que me pediram

e não há garantia de que você ainda possa usar a instância dessa 3ª lib após esse SyntaxError, é sua responsabilidade fornecer garantias ao usuário, digamos, restabelecendo o controle de terceiros depois que ele lançou

linha inferior, você precisa ser responsável por lidar com exceções internas ( nem todas elas, apenas as verificadas, eu imploro )

Estou entendendo o que você está dizendo, mas não concordo com isso.
Você está certo, é basicamente isso que estou dizendo.
Se eu usei uma biblioteca de terceiros que gera um erro, posso optar por lidar com isso ou ignorá-lo e deixar o usuário do meu código lidar com isso.
Existem muitas razões para fazer isso, por exemplo, a lib que estou escrevendo é agnóstica de interface do usuário, então não posso informar ao usuário que algo está errado, mas quem usa minha lib pode lidar com os erros que são lançados ao usar minha lib e manipulá-los interagindo com o usuário.

Se uma biblioteca ficar com um estado corrompido quando for lançada, provavelmente precisará documentá-lo.
Se eu usar essa biblioteca e, como resultado, em um erro, meu estado ficar corrompido, preciso documentá-la.

Linha inferior:
Esta sugestão vem para oferecer mais informações sobre erros lançados.
Ele não deve forçar os desenvolvedores a fazer as coisas de forma diferente, apenas facilitar para eles lidarem com os erros, se assim o desejarem.

você pode discordar, tudo bem, não vamos chamá-las de exceções verificadas, por favor, porque a maneira como você coloca não é o que as exceções verificadas são

vamos chamá-los de exceções listadas ou reveladas , porque tudo o que você se importa é tornar os desenvolvedores cientes deles

@aleksey-bykov
Justo, o nome mudou.

@aleksey-bykov

você não pode lidar com um problema que aconteceu 20 camadas abaixo normalmente, porque o estado interno está corrompido em 19 camadas e você não pode ir lá porque o estado é privado

Não, você não pode corrigir o estado interno, mas certamente pode corrigir o estado local, e esse é exatamente o objetivo de lidar com isso aqui e não mais profundamente na pilha.

Se o seu argumento é que não há como ter certeza em que estado alguns valores mutáveis ​​compartilhados estão ao lidar com a exceção, então é um argumento contra a programação imperativa e não se limita a esta proposta.

se cada camada for obrigada a assumir a responsabilidade de reagir a uma exceção que vem imediatamente de uma camada abaixo, há uma chance muito maior de uma recuperação bem-sucedida, essa é a ideia por trás das exceções verificadas como eu vejo

para colocar em palavras diferentes, exceções vindas de mais de 1 nível abaixo é uma frase, é tarde demais para fazer qualquer coisa além de reinstanciar toda a infraestrutura do zero (se você tiver sorte o suficiente, não há sobras globais que você possa' t alcance)

proposta como declarada é praticamente inútil, porque não há uma maneira confiável de reagir ao conhecimento de algo ruim que aconteceu fora do seu alcance

Isso é ótimo. FWIW: Acho que, se adicionado, deve ser necessário, por padrão, lidar com métodos de lançamento ou marcar seu método como lançamento também. Caso contrário, é apenas documentação praticamente.

@agonzalezjr
Eu acho que, como a maioria dos recursos no texto datilografado, você também deve poder optar por esse recurso.
Assim como não é obrigatório adicionar tipos, não deve ser obrigatório lançar/pegar.

Provavelmente deve haver um sinalizador para torná-lo obrigatório, como --onlyCheckedExceptions .

De qualquer forma, esse recurso também será usado para inferir/validar os tipos de exceções lançadas, portanto, não apenas para documentação.

@nitzantomer

Aqui está um exemplo:
Eu tenho um cliente da web no qual o usuário pode gravar/colar dados json e clicar em um botão.
O aplicativo pega essa entrada e a passa para uma biblioteca de terceiros que de alguma forma analisa esse json e retorna o json junto com os diferentes tipos de valores (string, number, boolean, array, etc).
Se esta biblioteca de terceiros lançar um SyntaxError eu posso recuperar: informe ao usuário que sua entrada é inválida e ele deve tentar novamente.

Esta é certamente uma área em que toda a ideia de exceções verificadas se torna obscura. É também onde a definição de _situação excepcional_ se torna obscura.
O programa em seu exemplo seria um argumento para JSON.parse ser declarado como lançando uma exceção verificada.
Mas e se o programa for um cliente HTTP e estiver chamando JSON.parse com base no valor de um cabeçalho anexado a uma resposta HTTP que contém um corpo mal formado? Não há nada significativo que o programa possa fazer para se recuperar, tudo o que ele pode fazer é relançar.
Eu diria que este é um argumento contra JSON.parse ser declarado como verificado.

Tudo depende do caso de uso.

Entendo que você está propondo que isso esteja sob um sinalizador, mas vamos imaginar que eu queira usar esse recurso, então habilitei o sinalizador. Dependendo do tipo de programa que estou escrevendo, isso pode me ajudar ou me atrapalhar.

Mesmo o clássico java.io.FileNotFoundException é um exemplo disso. Está verificado, mas o programa pode se recuperar? Realmente depende do que o arquivo ausente significa para o chamador, não para o chamado.

@aluanhaddad

Esta sugestão não propõe adicionar nenhuma nova funcionalidade, apenas adicionar uma forma de expressar em typescript algo que já existe em javascript.
Erros são lançados, mas atualmente o texto datilografado não tem como declará-los (ao lançar ou pegar).

Quanto ao seu exemplo, ao capturar o erro, o programa pode falhar "graciosamente" (por exemplo, mostrando ao usuário uma mensagem "algo deu errado") ao capturar esse erro, ou pode ignorá-lo, dependendo do programa/desenvolvedor.
Se o estado dos programas puder ser afetado por esse erro, o tratamento dele poderá manter um estado válido em vez de um quebrado.

De qualquer forma, o desenvolvedor deve decidir se pode se recuperar de um erro lançado ou não.
Também cabe a ele decidir o que significa recuperar, por exemplo, se eu estiver escrevendo este cliente http para ser usado como uma biblioteca de terceiros, posso querer que todos os erros lançados da minha biblioteca sejam do mesmo tipo:

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

Agora, na minha biblioteca, quando analiso a resposta usando JSON.parse , quero capturar um erro lançado e, em seguida, lançar meu próprio erro:

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

Se esse recurso for implementado, será fácil para mim declarar esse comportamento e ficará claro para os usuários da minha biblioteca como ele funciona e falha.

Esta sugestão não propõe adicionar nenhuma nova funcionalidade, apenas adicionar uma forma de expressar em typescript algo que já existe em javascript.

Eu sei. Estou falando dos erros que o TypeScript emitiria nesta proposta.
Minha suposição era que essa proposta implicava uma distinção entre especificadores de exceção verificados e não verificados (inferidos ou explícitos), novamente apenas para fins de verificação de tipo.

@aluanhaddad

O que você disse no comentário anterior:

Mas e se o programa for um cliente HTTP e estiver chamando JSON.parse com base no valor de um cabeçalho anexado a uma resposta HTTP que contém um corpo mal formado? Não há nada significativo que o programa possa fazer para se recuperar, tudo o que ele pode fazer é relançar.

Aplica o mesmo para retornar um null quando minha função é declarada para retornar um resultado.
Se o desenvolvedor escolher usar strictNullChecks então você pode dizer exatamente a mesma coisa se a função retornar um null (em vez de lançar) então no mesmo cenário "não há nada significativo que o programa possa fazer recuperar".

Mas mesmo sem usar um sinalizador onlyCheckedExceptions esse recurso ainda é útil porque o compilador irá reclamar, por exemplo, se eu tentar pegar o erro como string quando a função for declarada para lançar apenas Error .

Boa ideia, seria útil, mas não estrito/tipo seguro, pois não há como saber o que as chamadas aninhadas podem lançar em você.

Ou seja, se eu tiver uma função que possa lançar uma exceção do tipo A, mas por dentro eu chamo uma função aninhada e não a coloco em try catch - ela vai lançar sua exceção do tipo B para o meu chamador.
Portanto, se o chamador espera apenas exceções do tipo A, não há garantia de que ele não obterá outros tipos de exceções aninhadas.

(o tópico é muito longo então - desculpe se eu perdi este comentário)

@shaipetel
A proposição afirma que o compilador inferirá os tipos de erros não tratados e os adicionará à assinatura da função/método.
Portanto, no caso que você descreveu, sua função lançará A | B caso B não tenha sido tratado.

Ah eu vejo. Ele irá detalhar todos os métodos que eu chamo e coletar todos os tipos de exceção possíveis?
Eu adoraria ver isso acontecer, se for possível. Veja, um desenvolvedor sempre pode ter uma exceção inesperada que não será declarada, caso em que um "objeto não definido como instância" ou "dividir por 0" ou exceções semelhantes são sempre possíveis em quase qualquer função.
IMHO, teria sido melhor tratado como em C#, onde todas as exceções herdam de uma classe base que tem uma mensagem e não permite o lançamento de texto desempacotado ou outros objetos. Se você tem classe base e herança, você pode cascatear suas capturas e lidar com seu erro esperado em um bloco e outro inesperado em outro.

@shaipetel
Em javascript todos os erros são baseados na classe Error , mas você não está restrito a lançar erros, você pode lançar qualquer coisa:

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

Sim, eu sei como funciona o JavaScript, estamos discutindo o TypeScript que impõe mais limitações.
Eu sugeri, uma boa solução IMHO seria fazer o TypeScript seguir o tratamento de exceção que exige que todas as exceções lançadas sejam de uma classe base específica e não permitam lançar valores desempacotados diretamente.

Portanto, não permitirá "lançar 0" ou "lançar 'algum erro'".
Assim como o JavaScript permite muitas coisas que o TypeScript não permite.

Obrigado,

@nitzantomer

Aplica o mesmo para retornar um null quando minha função é declarada para retornar um resultado.
Se o desenvolvedor optar por usar strictNullChecks, você poderá dizer exatamente a mesma coisa se a função retornar um nulo (em vez de lançar) e, no mesmo cenário, "não há nada significativo que o programa possa fazer para recuperar".

Mas mesmo sem usar um sinalizador onlyCheckedExceptions, esse recurso ainda é útil porque o compilador reclamará, por exemplo, se eu tentar capturar o erro como uma string quando a função for declarada para lançar apenas Error.

Eu vejo o que você está dizendo, isso faz sentido.

@shaipetel como discutido anteriormente nesta proposta e em outros lugares, a subclasse de funções incorporadas como Error e Array não funciona. Isso leva a um comportamento diferente em tempos de execução e destinos de compilação amplamente usados.

A captura de valores de vários tipos sem erro é a maneira mais viável de visualizar a funcionalidade proposta. Na verdade, não vejo isso como um problema, pois os mecanismos de propagação de exceção que aproveitam esse recurso provavelmente resultariam em erros muito mais específicos e muito mais úteis do que, digamos, um rastreamento de pilha ou outras propriedades específicas de erro.
Estender Error não é viável. Não será viável até que todas as metas abaixo de es2015 não estejam mais em uso.

Se esta proposta leva indiretamente a mais subclasses da função Error então acho que é uma má ideia. Tal objeção é completamente separada de quaisquer objeções filosóficas sobre o uso de exceções para controle de fluxo ou a definição de circunstância excepcional. Portanto, se esta proposta fosse adotada, eu esperaria uma documentação extremamente barulhenta e direta sobre o uso correto e a necessidade de evitar subclasses Error .

Eu adoraria que houvesse algum tipo de lógica auxiliar para lidar com exceções digitadas, estou refatorando muito código de promessa para usar async/await, que atualmente se parece com:

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

Então no novo mundo fica assim:

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

O que é bom, mas requer mais código e é um pouco menos legível em alguns aspectos, então seria ótimo se houvesse alguma forma de suporte para:

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

O que produziria o bit anterior, mas como açúcar sintático, você pode até fazer como genérico, mas é um pouco mais desagradável:

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

@grofit
embora eu gostaria que o texto datilografado apoiasse o que você está sugerindo, não está relacionado a esse problema na minha opinião.
O que você está sugerindo pode até ser implementado sem implementar do que se trata esse problema, apenas que o compilador não poderá reclamar (por exemplo) que NotFoundError não foi lançado.

Eu pensei que isso era uma sugestão para capturas digitadas, não um problema de qualquer tipo, eu só não queria duplicar tópicos, vou postar em seu próprio problema então.

@grofit
As capturas digitadas também fazem parte da solicitação de recurso, mas também a capacidade de informar ao compilador que tipos de erros podem ser lançados a partir de funções (e então o compilador também poderá inferir quais são os tipos de erros que podem ser lançados).

Na minha opinião, as capturas digitadas podem ser implementadas sem as outras partes. Abra um novo problema, talvez a equipe do TS decida marcá-lo como duplicado, não sei.

@grofit

O que produziria o bit anterior, mas como açúcar sintático, você pode até fazer como genérico, mas é um pouco mais desagradável:

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

não vai funcionar porque implica a geração de código baseado apenas em tipos enquanto

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

passa a função Error em cada caso e provavelmente é implementado com instanceof . Código como esse é tóxico, pois incentiva a extensão Error que é uma coisa muito ruim.

@nitzantomer Concordo que é um problema separado. O OP tem exemplos de tipos de captura que não estendem Error .

@aluanhaddad
Para o que o @grofit pede, você pode fazer algo como:

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

Onde isXXX(error) são guardas de tipo da forma de:
function isXXX(error): error is XXX { ... }

@nitzantomer com certeza, mas os guardas de tipo não são o problema. A questão é

class MyError extends Error {
  myErrorInfo: string;
}

o que é problemático, mas já foi discutido aqui. Não quero entrar em detalhes, mas muitas bases de código grandes e conhecidas foram impactadas negativamente pela adoção dessa prática ruim. Estender Error é uma má ideia.

@aluanhaddad
Estou ciente disso, e é por isso que ofereci uma maneira de realizar o que o @grofit solicitou, mas sem a necessidade de estender Error . Um desenvolvedor ainda poderá estender Error se quiser, mas o compilador poderá gerar código js que não precisa usar instanceof

@nitzantomer Percebo que você está ciente, mas não percebi o que você estava sugerindo para @grofit , o que parece uma boa ideia.

Estou batendo em um cavalo morto porque não gosto de ter que lidar com APIs que querem me fazer usar esses padrões. Independentemente disso, me desculpe se eu tirei essa discussão do tópico.

Houve mais alguma reflexão sobre esta discussão? Eu adoraria ver uma cláusula throws em texto datilografado.

@aluanhaddad Você continua dizendo que estender Error é uma prática ruim. Você pode elaborar um pouco sobre isso?

Foi discutido longamente. Basicamente, você não pode estender de forma confiável os tipos internos. O comportamento varia drasticamente na combinação de ambientes e configurações --target .

A única razão pela qual perguntei é porque seus comentários foram os primeiros que ouvi sobre isso. Depois de pesquisar um pouco, só encontrei artigos explicando como e por que _should_ extend Error .

Não, não vai funcionar. Você pode ler sobre isso em detalhes em https://github.com/Microsoft/TypeScript/issues/12123 e nos vários problemas vinculados.

Funcionará, mas apenas em "ambientes específicos", que estão se tornando a maioria à medida que o ES6 está sendo adaptado.

@nitzantomer : Você acha que é possível implementar uma verificação TSLint que informa o desenvolvedor quando uma função com @throws é chamada sem um try/catch ao redor?

@bennyn Sou usuário do TSLint, mas nunca pensei em criar novas regras.
Eu realmente não sei se é possível (embora eu ache que seja), e se for, quão fácil.

Se você tentar, por favor atualize aqui, obrigado.

@shaipetel (e também @nitzantomer)

Ré:

Sim, eu sei como funciona o JavaScript, estamos discutindo o TypeScript que impõe mais limitações.
Eu sugeri, uma boa solução IMHO seria fazer o TypeScript seguir o tratamento de exceção que exige que todas as exceções lançadas sejam de uma classe base específica e não permitam lançar valores desempacotados diretamente.
Portanto, não permitirá "lançar 0" ou "lançar 'algum erro'".
Assim como o JavaScript permite muitas coisas que o TypeScript não permite.

Não tenho certeza se isso é o que você estava sugerindo @shaipetel , mas apenas no caso ... Eu advertiria contra o Typescript restringir throw para apenas retornar Error s. Os próximos recursos de renderização assíncrona do React funcionam nos bastidores por throw ing Promise s! (Parece estranho, eu sei... mas eu li que eles avaliaram isso versus async / await e yield / yield* geradores para seu caso de uso e tenho certeza que sabem o que estão fazendo!)

throw ing Error s é tenho certeza que 99% dos casos de uso lá fora, mas não 100%. Eu não acho que o Typescript deveria restringir os usuários a apenas throw ing coisas que estendem uma classe base Error . Certamente é muito mais avançado, mas throw tem outros casos de uso além de erros/exceções.

@mikeleonard
Concordo, existem muitos exemplos de código js existente que lança uma variedade de tipos (eu vi muito throw "error message string" ).
O novo recurso React é outro bom exemplo.

Apenas meus dois centavos, estou convertendo código para async/await e, portanto, estou sendo submetido a lançamento (como um aparte, odeio exceções). Ter uma cláusula throws como esta questão discute seria bom na minha opinião. Eu acho que seria útil também, mesmo que "joga qualquer" fosse permitido. (Além disso, talvez uma opção "nothrows" e do compilador que padronize as coisas para "nothrows".)

Parece uma extensão natural para permitir digitar o que uma função lança. Os valores de retorno podem ser opcionalmente digitados em TS, e para mim parece que throw é apenas outro tipo de retorno (como evidenciado por todas as alternativas sugeridas para evitar throw, como https://stackoverflow.com/a/39209039/162530).

(Pessoalmente, eu também adoraria ter a opção de compilador (opcional) para impor que qualquer chamador para uma função declarada como throws também deve declarar como throws ou capturá-los.)

Meu caso de uso atual: não quero converter toda a minha base de código [Angular] para usar exceções para tratamento de erros (já que eu as odeio). Eu uso async/await nos detalhes de implementação das minhas APIs, mas converto throw para Promises/Observables normais quando uma API retorna. Seria bom que o compilador verificasse se estou capturando as coisas certas (ou capturando-as, de preferência).

@aleksey-bykov Não vamos mudar a natureza do JavaScript, vamos apenas adicionar digitação a ele. :)

adicionar digitação já o altera (corta o código que não faz sentido),
da mesma forma que podemos melhorar o tratamento de exceções

Em qui, 26 de julho de 2018, 20h22, Joe Pea [email protected] escreveu:

@aleksey-bykov https://github.com/aleksey-bykov Não vamos mudar o
natureza do JavaScript, vamos apenas adicionar digitação a ele. :)


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/Microsoft/TypeScript/issues/13219#issuecomment-408274156 ,
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/AA5PzfUNS5E093Z74WA4WCUaTyWRRZC3ks5uKl1FgaJpZM4LXwLC
.

Para promessas, se você usar o encadeamento then em vez de async/await e evitar usar throw , tudo funciona muito bem na verdade. Aqui está um esboço de prova de conceito:

https://bit.ly/2NQZD8i - link do playground

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

O predicado catch remove tipos da união, enquanto o retorno de reject() adiciona a eles. A única coisa que não funciona é a inferência de tipos de parâmetros de união (provavelmente um bug)

Problemas abordados no exemplo acima:

  • não está restrito a erros, você pode rejeitar com qualquer coisa (embora provavelmente seja uma má ideia)
  • não está restrito ao uso de instanceof, você pode usar qualquer guarda de tipo de predicado para remover erros da união

O principal problema que vejo usando isso para todo o código são as assinaturas de retorno de chamada, para fazê-lo funcionar de maneira compatível, o "tipo de retorno" padrão seria "might-throw-any", e se você quiser restringir isso, diga throws X ou throws never se não, por exemplo

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

A versão sem a assinatura:

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

na verdade seria padrão

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

para garantir que todo o código atual seja compilado.

Ao contrário de strictNullChecks , onde os valores de retorno nulos são bastante incomuns, acho que as exceções em JS são bastante difundidas. Modelá-los em arquivos .d.ts pode não ser tão ruim (importar tipos de dependências para descrever seus erros), mas definitivamente não será um esforço trivial e resultará em grandes uniões.

Eu acho que um bom meio termo seria focar em Promises e async/await , já que uma Promise já é um wrapper, e o código assíncrono é onde o tratamento de erros se ramifica mais em cenários típicos. Outros erros seriam "desmarcados"

@spion-h4 isso (já/vai) pode ser usado em texto datilografado?

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

@bluelovers
não, o texto datilografado atualmente não suporta a palavra-chave throws , é disso que se trata este problema.

Algo a ser observado (não que necessariamente bloqueie esta proposta) é que sem classes nominais, digitar erros internos ainda não é particularmente útil, pois todos são estruturalmente iguais.

por exemplo:

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
Esta questão não tem nada a ver com a proposta.
Você pode entrar no mesmo problema exato fazendo isso:

class BaseClass {
    propA!: string;
}

class MyClass1 extends BaseClass { }

class MyClass2 extends BaseClass { }

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

É uma limitação de linguagem que afeta muitos casos de uso.

Há uma ótima leitura de @ahejlsberg sobre exceções digitadas: https://www.artima.com/intv/handcuffs.html

Acredito que o TypeScript está em uma boa posição para evitar esses problemas. O TypeScript tem tudo a ver com soluções _pragmáticas_ para problemas do _mundo real_. Na minha experiência, em grandes bases de código JavaScript e TypeScript, o tratamento de erros é um dos maiores problemas - ver quais funções de erros _poderiam_ lançar e eu _poderia_ querer manipular é incrivelmente difícil. Isso só é possível lendo e escrevendo uma boa documentação (todos nós sabemos como somos bons nisso /s) ou olhando para a implementação, e como ao contrário dos valores de retorno, as exceções apenas se propagam através de chamadas de função automaticamente, não é suficiente apenas verifique a função chamada diretamente, mas para pegá-los todos, você teria que verificar as chamadas de função aninhadas também. _Este é um problema do mundo real_.

Erros em JavaScript são, na verdade, valores muito úteis. Muitas APIs no NodeJS lançam objetos de erro detalhados, que possuem códigos de erro bem definidos e expõem metadados úteis. Por exemplo, erros de child_process.execFile() têm propriedades como exitCode e stderr , erros de fs.readFile() têm códigos de erro como ENOENT (arquivo não encontrado ) ou EPERM (permissões insuficientes). Conheço muitas bibliotecas que fazem isso também, por exemplo, o driver de banco de dados pg fornece metadados suficientes em um erro para saber qual restrição de coluna exata causou a falha de um INSERT .

Você pode ver uma quantidade preocupante de verificações de regex frágeis em mensagens de erro em bases de código porque as pessoas não estão cientes de que os erros têm códigos de erro adequados e quais são eles.

Se pudéssemos definir em declarações @types e lib.d.ts quais erros essas funções podem lançar, o TypeScript teria o poder de nos ajudar com a estrutura do erro - quais possíveis erros podem existir, qual o valores de código de erro são, quais propriedades eles têm. Isso _não_ é sobre exceções tipadas e, portanto, evita completamente todos os problemas com exceções tipadas. É uma _solução pragmática_ para um _problema do mundo real_ (ao contrário de dizer às pessoas para usarem valores de retorno de erro monádico - a realidade do JavaScript parece diferente, funções lançam erros).

A anotação pode ser completamente opcional (ou obrigatória com um sinalizador do compilador). Se não for especificado em um arquivo de declaração, uma função simplesmente lança any (ou unknown ).
Uma função pode declarar manualmente para nunca lançar com throws never .
Se a implementação estiver disponível, uma função lança uma união de todos os tipos de exceção das funções que ela chama e suas próprias instruções throw que não estão dentro de um bloco try com um catch cláusula
Se um deles lançar any , a função lançará any também - tudo bem, em cada limite de função o desenvolvedor tem a oportunidade de corrigi-lo através de uma anotação explícita.
E em muitos casos, onde uma única função bem conhecida é chamada e envolvida em um try/catch (por exemplo, lendo um arquivo e manipulando-o não sendo encontrado), o TypeScript pode então inferir o tipo na cláusula catch.

Não precisamos da subclasse Error para isso nem instanceof - os tipos de erro podem ser interfaces, que especificam códigos de erro com literais de string, e o TypeScript pode discriminar a união no código de erro ou usar guardas de tipo.

Definindo tipos de erro

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

Além dos erros, também é possível marcar outros tipos de efeitos colaterais como

  • Divergência (loop infinito / não retornando)
  • IO
  • Não-determinismo ( Math.random )

Isso me lembra Koka do MSR, que pode marcar efeitos em tipos de retorno.

A proposta:

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

Eu amo a ideia de declaração de tipo de erro! Isso seria uma grande ajuda para as pessoas que precisam e não fará nada de ruim para as pessoas que não gostam. Eu tenho agora o problema no meu projeto de nó, que poderia ser resolvido mais rapidamente com esse recurso. Eu tenho que pegar erros e enviar o código http adequado de volta - para fazer isso eu tenho que sempre verificar todas as chamadas de funções para descobrir as exceções que eu tenho que lidar - e isso não é divertido -_-

PS. A sintaxe em https://github.com/Microsoft/TypeScript/issues/13219#issuecomment -428696412 parece muito boa - &! e envolvendo tipos de erro em <> simplesmente incrível.

Também sou a favor disso. Agora todos os erros não são digitados e isso é muito chato. Embora eu espere que a maior parte do throws possa ser derivada do código, pois temos que escrevê-lo em todos os lugares.

Dessa forma, as pessoas podem até escrever regras tslint que forçam o desenvolvedor a detectar erros hostis ao usuário em terminais de descanso etc.

Fiquei realmente surpreso que essa funcionalidade não estava _já_ dentro do TypeScript. Uma das primeiras coisas que fui declarar. Estaria tudo bem se o compilador o aplicasse ou não, contanto que você pudesse obter as informações de que haverá um erro lançado e que tipo de erros serão lançados.

Mesmo se não obtivermos o &! e apenas obtermos o Throws<T> que seria :+1:

Isso é exatamente o que eu tenho em mente, seria muito mais bonito se o TypeScript suportasse essas cláusulas.

Outro recurso possível seria forçar de alguma forma (talvez no nível da função ou no nível do módulo) strictExceptionTracking - o que significaria que você teria que invocar qualquer coisa declarada como throws X com uma expressão try! invokeSomething() como em Rust e rápido.

Ele simplesmente compilará para invokeSomething() mas a possibilidade de exceções será visível no código, tornando mais fácil identificar se você está deixando algo mutável em um estado transitório ruim (ou deixando recursos alocados indispostos)

@be5invis

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

Estou apenas dando meus dois centavos aqui, mas o &! parece incrivelmente feio para mim.
Eu acho que as pessoas que são novas no texto datilografado ficariam meio assustadas com esse símbolo, é realmente pouco convidativo.
Um simples throws é mais explícito, intuitivo e simples, na minha opinião.

Fora isso, +1 para exceções digitadas. 👍

Eu gostaria de adicionar meu +1 para exceções digitadas e verificadas.
Eu fiz um ótimo uso de exceções tipadas em meu código. Estou totalmente ciente das armadilhas de instanceof , eu realmente usei isso para minha vantagem ao poder escrever manipuladores genéricos para erros relacionados que herdam de uma classe base comum. A maioria dos outros métodos que encontrei para evitar a herança da classe base Error acabam sendo (pelo menos) igualmente complexos e problemáticos de diferentes maneiras.
As exceções verificadas são uma melhoria que, na minha opinião, se presta a uma maior percepção da análise estática na base de código. Em uma base de código de complexidade suficiente, pode ser fácil perder quais exceções são lançadas por uma função específica.

Para aqueles que estão procurando por segurança de erros em tempo de compilação no typescript, você pode usar minha biblioteca ts-results .

@vultix para mim, infelizmente, a separação não é clara o suficiente para colocar isso em uma configuração de equipe.

@vultix A abordagem da sua lib foi discutida acima e é exatamente o oposto do que esse recurso está aqui para resolver.
Javascript já tem o mecanismo de tratamento de erros, é chamado de exceções, mas o typescript não tem uma forma de descrevê-lo.

@nitzantomer Concordo completamente que esse recurso é uma necessidade para o texto datilografado. Minha biblioteca nada mais é do que um substituto temporário até que esse recurso seja adicionado.

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

Eu sei que estou um pouco atrasado para este jogo sobre isso, mas o abaixo parece um pouco mais sintaxe Typescripty do que vírgula.

Iterable<number> throws TypeError | RangeError

Mas eu ainda sou bom com vírgula. Eu só gostaria que tivéssemos isso no idioma.
A principal é que eu gostaria em JSON.parse() porque muitos dos meus colegas de trabalho parecem esquecer que JSON.parse pode gerar um erro e isso economizaria muitas idas e vindas com puxar solicitações.

@WORMSS
Eu concordo completamente.
Minha proposta incluiu a sintaxe que você recomenda:

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

Podemos seguir o padrão de declaração de erro de uma linguagem OOP como Java? A palavra-chave "throws" é usada para declarar que precisamos "try..catch" ao usar uma função com um erro potencial

@allicanseenow
A proposta é para uso opcional da cláusula throws.
Ou seja, você não precisará usar try/catch se estiver usando uma função que lança.

@allicanseenow você pode querer ler meu artigo acima para contextualizar, incluindo o artigo vinculado: https://github.com/microsoft/TypeScript/issues/13219#issuecomment -416001890

Eu acho que isso é uma das coisas que faltam no sistema de tipos de texto datilografado.
É puramente opcional, não altera o código emitido, ajuda a trabalhar com bibliotecas e funções nativas.

Também saber quais erros podem ser lançados pode permitir que os editores "autocompletem" a cláusula catch com condições if para lidar com todos os erros.

Quero dizer, mesmo aplicativos pequenos merecem tratamento de erros adequado - o pior agora é que os usuários não sabem quando algo pode dar errado.

Para mim, este é o recurso ausente do TOP agora.

@RyanCavanaugh , houve muitos comentários desde que o rótulo "Awaiting More Feedback" foi adicionado. Algum membro da equipe datilografada pode contribuir?

@nitzantomer Eu apoio a ideia de adicionar lançamentos ao TypeScript, mas um try/catch opcional não permitiria declarações de lançamentos potencialmente imprecisas quando usadas em funções aninhadas?

Para um desenvolvedor confiar que a cláusula throws de um método é precisa, eles teriam que fazer a perigosa suposição de que em nenhum ponto da hierarquia de chamadas desse método houve uma exceção opcional ignorada e lançada na pilha não relatada. Acho que @ajxs pode ter aludido a isso no final de seu comentário, mas acho que isso seria um grande problema. Especialmente com a fragmentação da maioria das bibliotecas npm.

@ConnorSinnott
Espero ter entendido bem:

O compilador inferirá tipos lançados, por exemplo:

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

Na verdade, será function fn(): throws string { ... } .
O compilador também dará erro quando houver uma incompatibilidade entre os erros declarados e os reais:

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

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

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

O compilador deve reclamar que fn2 lança string | MyError e não string .

Com tudo isso em mente, não vejo como essa suposição é mais perigosa do que a suposição de que outros tipos declarados nos quais um desenvolvedor confia ao usar outras bibliotecas, estruturas etc.
Não tenho certeza se realmente cobri todas as opções, e ficarei feliz se você puder apresentar um cenário interessante.

E mesmo com esse problema, é praticamente o mesmo que agora:

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

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

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

    a_fancy_3rd_party_lib_function();
}

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

Podemos ter o typescript nos protegendo até disso, mas exigirá uma sintaxe um pouco diferente do javascript:

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

Isso será compilado para:

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

Ei! Obrigado por responder! Na verdade, ter os lances inferidos como você mencionou atenua esse problema que eu estava pensando. Mas o segundo exemplo que você listou é interessante.

Dado

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

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

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

Para eu declarar explicitamente MyVeryImportantError, eu também teria que declarar explicitamente todos os erros adicionais da pilha de chamadas, o que pode ser um punhado dependendo da profundidade do aplicativo. Além disso, potencialmente tedioso. Eu certamente não gostaria de mergulhar em toda a cadeia de chamadas para gerar uma lista de possíveis erros que podem surgir no caminho, mas talvez o IDE possa ajudar.

Eu estava pensando em propor algum tipo de operador de spread para permitir que o desenvolvedor declarasse explicitamente seu erro e apenas jogasse o restante.

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

Mas haveria uma maneira mais fácil de obter o mesmo resultado: basta descartar a declaração throws.

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

O que levanta a questão: quando eu veria um benefício em usar a palavra-chave throws em vez de apenas deixar o texto datilografado inferir os erros?

@ConnorSinnott

Não é diferente de outras linguagens que usam a cláusula throw, ou seja, java.
Mas acho que na maioria dos casos você não terá que lidar com muitos tipos. No mundo real você normalmente lidará com apenas string , Error (e subclasses) e objetos diferentes ( {} ).
E, na maioria dos casos, você poderá usar uma classe pai que captura mais de um tipo:

type MyBaseError = {
    message: string;
};

type CodedError = MyBaseError & {
    code: number;
}

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

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

    fn1();
}

Quanto ao uso implícito da cláusula throw versus o compilador inferir, acho que é como declarar tipos no texto datilografado, em muitos casos você não precisa, mas pode fazer isso para documentar melhor seu código para que quem ler depois pode entender melhor.

Outra abordagem se você deseja segurança de tipo é apenas tratar erros como valores à la Golang:
https://gist.github.com/brandonkal/06c4a9c630369979c6038fa363ec6c83
Ainda assim, este seria um bom recurso para ter.

@brandonkal
Essa abordagem já foi discutida anteriormente no tópico.
É possível, mas fazer isso é ignorar uma parte/recurso inerente que o javascript nos permite.

Também acho que o especificador noexcept (#36075) é a solução mais simples e melhor no momento, pois para a maioria dos programadores, lançar exceções considera um antipadrão.

Aqui estão alguns recursos interessantes:

  1. A verificação de funções não gerará um erro:
noexcept function foo(): void {
  throw new Error('Some exception'); // <- compile error!
}
  1. Aviso sobre blocos try..catch redundantes:
try { // <- warning!
  foo();
} catch (e) {}
  1. Resolver sempre Promessas:
interface ResolvedPromise<T> extends Promise<T> {
  // catch listener will never be called
  catch(onrejected?: noexcept (reason: never) => never): ResolvedPromise<T>; 

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

@moshest

como para a maioria dos programadores, lançar exceções considera um antipadrão.

Acho que não faço parte desse grupo da "maioria dos programadores".
O noexcept é legal e tudo, mas não é sobre isso que se trata.

Se as ferramentas estiverem lá, eu as usarei. throw , try/catch e reject estão lá, então vou usá-los.

Seria bom tê-los devidamente digitados no texto datilografado.

Se as ferramentas estiverem lá, eu as usarei. throw , try/catch e reject estão lá, então vou usá-los.

Certo. Vou usá-los também. Meu ponto é que noexcept ou throws never será um bom começo para esta questão.

Exemplo idiota: eu quero saber que se eu chamar Math.sqrt(-2) ele nunca irá lançar nenhum erro.

O mesmo se aplica a bibliotecas de terceiros. Dá mais controle sobre o código e os casos extremos que preciso lidar como programador.

@moshest
Mas por que?
Esta proposta inclui todos os benefícios da proposta 'noexpect', então por que se contentar com menos?

Porque leva muito tempo (essa edição tem 3 anos), e eu esperava que pelo menos pudéssemos começar com o recurso mais básico primeiro.

@moshest
Bem, eu prefiro um recurso melhor que demore mais do que uma solução parcial que levará menos tempo, mas ficaremos presos a ele para sempre.

Eu sinto que noexcept pode ser facilmente adicionado mais tarde, na verdade, poderia ser um tópico separado.

noexcept é o mesmo que throws never .

TBH Eu acho que é mais importante ter algum mecanismo garantindo o tratamento de exceções (ala não usado error em Go) em vez de fornecer dicas de tipo para try/catch

@roll não deve haver nenhuma "garantia".
não é obrigatório lidar com exceções em javascript e também não deve ser obrigatório em typescript.

Pode ser uma opção como parte do modo estrito do typescrypt que uma função deve capturar o erro ou declará-lo explicitamente throws e passá-lo adiante

Para adicionar meus 5 centavos: vejo exceções semelhantes a uma função retornando null : É algo que você normalmente não espera que aconteça. Mas se isso acontecer, ocorre um erro em tempo de execução, o que é ruim. Agora, o TS adicionou "tipos não anuláveis" para lembrá-lo de lidar com null . Acho que adicionar throws leva esses esforços um passo adiante, lembrando você de lidar com exceções também. É por isso que acho que esse recurso é definitivamente necessário.

Se você observar o Go, que retorna erros em vez de lançá-los, poderá ver ainda mais claramente que os dois conceitos não são tão diferentes. Além disso, ajuda você a entender algumas APIs mais profundamente. Talvez você não saiba que algumas funções podem lançar e você percebe isso muito mais tarde na produção (por exemplo JSON.parse , quem sabe quantas mais existem?).

@obedm503 Esta é realmente uma filosofia do TS: por padrão, o compilador não reclama de nada. Você pode habilitar opções para tratar certas coisas como um erro ou habilitar o modo estrito para habilitar todas as opções de uma vez. Então, isso deve ser dado.

Eu amo este recurso sugerido.
A digitação e inferência de exceção podem ser uma das melhores coisas em toda a história da programação.
❤️

Olá, acabei de passar um pouco de tempo tentando encontrar uma solução alternativa com o que temos atualmente no TS. Como não há como obter o tipo dos erros lançados dentro de um escopo de função (nem de obter o tipo do método atual), descobri como poderíamos definir explicitamente os erros esperados dentro do próprio método. Poderíamos mais tarde, recuperar esses tipos e pelo menos saber o que poderia ser lançado dentro do método.

Aqui está o meu 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)

Muito obrigado pelo seu feedback positivo! Eu percebo que poderia ser mais fácil refatorar o código existente sem ter que envolver todo o tipo retornado no tipo throwable . Em vez disso, poderíamos apenas anexar esse ao tipo retornado, o código a seguir permite anexar os erros jogáveis ​​da seguinte maneira:

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

Esta é a única mudança para a parte do exemplo, aqui está a nova declaração de tipos:

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

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

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

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

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

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

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

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

Também adicionei um novo tipo exceptionsOf que permite extrair os erros de uma função para escalar a responsabilidade. Por exemplo:

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

Como exceptionsOf obtém uma união de erros, você pode escalar quantos métodos críticos desejar:

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

Eu não gosto do uso de typeof , se eu encontrar uma maneira melhor eu te aviso

Você pode testar aqui as dicas de resultados : passe typedError na linha 106

@Xample Essa é uma ótima solução com as ferramentas que temos agora!
Mas acho que na prática não é suficiente, pois você ainda pode fazer coisas como:

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

E o tipo de b é inferido como número que está correto, mas apenas se estiver dentro de uma tentativa
Isso não deve ser válido

@luisgurmendezMLabs Não entendi muito bem o que você quer dizer. Se você seguir o repositório do @Xample na linha 56. Você pode ver que o resultado de myNumber + 23 é inferido como number , enquanto myNumber: number & extraType<Error | CustomError> .

Em outra fase, a em seu exemplo nunca é envolvido em algo como um Try Monad. Ele não tem wrapper além de uma interseção com & extraType<Error | CustomError> .

Parabéns pelo design incrível @Xample 👏👏👏👏👏👏. Isso é realmente promissor e já é útil mesmo sem nenhum açúcar sintático. Você tem algum plano para construir uma biblioteca de tipos para isso?

@ivawzh Meus pensamentos são que, na prática, isso pode trazer alguns problemas, pois:

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

Esse retorno do tipo de função é inferido como número e isso não é totalmente correto

@luisgurmendezMLabs o tipo throwable está dentro do tipo de retorno da função apenas como uma maneira de colar o tipo do erro com a função (e para poder recuperar esses tipos posteriormente). Eu nunca retorno erros de verdade, eu apenas os jogo. Portanto, se você estiver dentro de um bloco try ou não, não mudará nada.

@ivawzh obrigado por perguntar, acabei de fazer isso por você

@luisgurmendezMLabs ah ok, entendo seu ponto, parece que o texto datilografado apenas infere o primeiro tipo encontrado. Por exemplo, se você tivesse:

function stillARiskyMethod() { 
    return superRiskyMethod();
}

o tipo de retorno de stillARiskyMethod seria inferido corretamente, enquanto

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

apenas digitaria o tipo de retorno como superRiskyMethod() e descartaria o de anotherSuperRiskyMethod() . não investiguei mais

Por esse motivo, você precisa escalar manualmente o tipo de erro.

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

Eu também só queria deixar meus pensamentos sobre isso.

Eu acho que isso seria um recurso muito bom para implementar, pois existem diferentes casos de uso para querer retornar um Error/null e querer lançar algo e pode ter muito potencial. De qualquer forma, lançar exceções faz parte da linguagem Javascript, então por que não devemos dar a opção de digitar e inferir isso?

Por exemplo, se estou fazendo muitas tarefas que podem dar erro, acho inconveniente ter que usar uma instrução if para verificar o tipo de retorno a cada vez, quando isso pode ser simplificado usando um try/catch onde, se houver uma dessas tarefas for lançada, ela será tratada no catch sem nenhum código adicional.

Isso é particularmente útil quando você vai tratar os erros da mesma maneira ; por exemplo, em express/node.js, talvez eu queira passar o erro para o NextFunction (manipulador de erro).

Em vez de fazer if (result instanceof Error) { next(result); } cada vez, eu poderia simplesmente envolver todo o código para essas tarefas em um try/catch, e no meu catch eu sei que desde que uma exceção foi lançada, sempre vou querer passar isso para o meu manipulador de erros, assim pode catch(error) { next(error); }

Também não vi isso discutido ainda (pode ter perdido, no entanto, este tópico tem alguns comentários!), mas se isso fosse implementado, seria obrigatório (ou seja: erro de compilação) ter uma função que lança sem usar o throws cláusula em sua declaração de função? Eu sinto que isso seria bom de fazer (não estamos forçando as pessoas a lidar com os lançamentos, apenas informa que a função é lançada), mas a grande preocupação aqui é que, se o Typescript fosse atualizado dessa maneira, provavelmente quebrar lotes de código atualmente existente.

Edit: Outro caso de uso que pensei poderia ser isso também ajudaria na geração de JSDocs usando a tag @throws

Vou repetir aqui o que disse na questão agora comprometida.


Eu acho que o typescript deve ser capaz de inferir o tipo de erro da maioria das expressões JavaScript. Permitindo uma implementação mais rápida pelos criadores da biblioteca.

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();
}
  • Função a sabemos que o tipo de erro é 'unlucky' , e se quisermos ser muito cautelosos podemos estendê-lo para Error | 'unlucky' , portanto includeError .
  • A função b herdará o tipo de erro da função a .
  • A função c é quase idêntica; 'unlucky' | 'fairly lucky' ou Error | 'unlucky' | 'fairly lucky' .
  • A função d terá que lançar unknown , já que eval é... desconhecido
  • A função e captura o erro de d , mas como há throw no bloco catch, inferimos seu tipo 'too bad...' , aqui, pois o bloco apenas contém throw 'primitive value' podemos dizer que não pode lançar Error (me corrija se eu perdi alguma magia negra JS...)
  • A função f herda de c da mesma forma que b #$ herda de $ a .
  • A função g herda 'unlucky' de a e unknown de c assim 'unlucky' | unknown => unknown

Aqui estão algumas opções do compilador que acho que devem ser incluídas, pois o envolvimento dos usuários com esse recurso pode variar dependendo de sua habilidade, bem como da segurança de tipo das bibliotecas das quais dependem em determinado projeto:

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

Quanto à sintaxe de como expressar o erro que qualquer função pode produzir, não tenho certeza, mas sei que precisamos da capacidade de ser genérico e inferível.

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;

Meu caso de uso, como mostrar um caso de uso real, pode mostrar outro que tem um valor:

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

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

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

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

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

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

Eu preciso de um coletor de erros para evitar o envio de cabeçalhos de resposta várias vezes para vários erros (eles geralmente não acontecem, mas quando o fazem em massa!)

Este problema ainda está marcado com Awaiting More Feedback , o que podemos fazer para fornecer mais comentários ? Este é o único recurso que invejo a linguagem java

Temos um caso de uso em que lançamos um erro específico quando uma chamada de API retorna um código diferente de 200:

interface HttpError extends Error {
  response: Response
}

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

Não poder digitar o bloco catch acaba fazendo com que os desenvolvedores esqueçam que 2 tipos possíveis de erros podem ser lançados, e eles precisam lidar com ambos.

Eu prefiro sempre lançar o objeto Error:

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

Por que esse recurso ainda "não há feedback suficiente"? É tão útil ao invocar a API do navegador como indexedDB, localstorage. Isso causou muitas falhas em cenários complexos, mas o desenvolvedor não pode saber na programação.

Hegel parece ter esse recurso perfeitamente.
https://hegel.js.org/docs#benefits (role até a seção "Erro de digitação")

Desejo que o TypeScript tenha um recurso semelhante!

DL;DR;

  • A função de rejeição das promessas deve ser digitada
  • Qualquer tipo lançado dentro de um bloco try deve ser inferido usando uma união no argumento de erro do catch
  • error.constructor deve ser digitado corretamente usando o tipo real e não apenas any para evitar a perda de um erro lançado.

Ok, talvez devêssemos simplesmente esclarecer quais são nossas necessidades e expectativas:

Erros geralmente são tratados de 3 maneiras em js

1. O caminho do nó

Usando retornos de chamada (que podem realmente ser digitados)

Exemplo de uso:

import * as fs from 'fs';

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

Onde fs.d.ts nos dá:

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

Portanto, o erro é digitado assim

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

2. O caminho da promessa

Onde a promessa é resolvida ou rejeitada, enquanto você pode digitar o value resolvido, você não pode digitar a rejeição geralmente chamada de reason .

Aqui está a assinatura do construtor de uma promessa: Observe que o motivo é digitado any

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

Teoricamente, poderia ter sido possível digitá-los da seguinte forma:

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

Dessa forma, teoricamente ainda poderíamos fazer uma conversão 1-1 entre um callback do nó e uma promessa, mantendo toda a tipagem ao longo desse processo. Por exemplo:

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. A maneira "tentar e pegar"

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

Apesar de estarmos lançando um erro "Error" 2 linhas antes do bloco catch, o TS não consegue digitar o error (valor) como Error (tipo).

Não é o caso de usar promessas dentro de uma função async (não há mágica "ainda"). Usando nosso retorno de chamada de nó prometido:

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

Não há informações ausentes para que o typescript seja capaz de prever que tipo de erro pode ser gerado em um escopo try.
Devemos, no entanto, considerar funções nativas internas que podem gerar erros que não estão dentro do código-fonte, no entanto, se toda vez que uma palavra-chave "throw" estiver na fonte, o TS deve coletar o tipo e sugeri-lo como um possível tipo para o erro. Esses tipos, obviamente, seriam delimitados pelo bloco try .

Este é apenas o primeiro passo e ainda haverá espaço para melhorias, como um modo estrito forçando o TS a funcionar como em Java, ou seja, forçar o usuário a usar um método arriscado (um método que pode lançar algo) dentro de um try bloco function example throw ErrorType { ... } para aumentar a responsabilidade de lidar com os erros.

Por último, mas não menos importante: evite perder um erro

Qualquer coisa pode ser lançada, não apenas um Error ou até mesmo uma instância de um objeto. Significando o seguinte é válido

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

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

Saber que o erro pode ser do tipo number | Error seria incrivelmente útil. No entanto, para evitar esquecer de lidar com um possível tipo de erro, não é realmente a melhor ideia usar blocos if/else separados sem um conjunto estrito de resultados possíveis.
No entanto, um switch case faria isso muito melhor, pois podemos ser avisados ​​se esquecermos de corresponder a um caso específico (qual seria o fallback para a cláusula padrão). Não podemos (a menos que façamos algo hackish) mudar o caso de um tipo de instância de objeto, e mesmo se pudéssemos, podemos lançar qualquer coisa (não apenas um objeto, mas também um booleano, uma string e um número que não tenha "instância"). No entanto, podemos usar o construtor da instância para descobrir qual é o tipo. Agora podemos reescrever o código acima da seguinte forma:

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… o único problema restante é que o TS não digita o error.constructor e, portanto, não há como restringir o caso do switch (ainda?), se o fizesse, teríamos uma linguagem de erro tipado segura para js.

Por favor, comente se você precisar de mais feedback

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

blendsdk picture blendsdk  ·  3Comentários

kyasbal-1994 picture kyasbal-1994  ·  3Comentários

siddjain picture siddjain  ·  3Comentários

seanzer picture seanzer  ·  3Comentários

zhuravlikjb picture zhuravlikjb  ·  3Comentários