typescript 类型系统在大多数情况下很有用,但在处理异常时无法使用。
例如:
function fn(num: number): void {
if (num === 0) {
throw "error: can't deal with 0";
}
}
这里的问题有两个(不看代码):
在许多情况下,这些并不是真正的问题,但了解函数/方法是否可能引发异常在不同的情况下非常有用,尤其是在使用不同的库时。
通过引入(可选的)检查异常,类型系统可以用于异常处理。
我知道检查的异常并没有达成一致(例如Anders Hejlsberg ),但是通过使其成为可选的(也许是推断出来的?稍后再说),它只是增加了添加更多关于代码的信息的机会,这些信息可以帮助开发人员、工具和文档。
它还将允许在大型项目中更好地使用有意义的自定义错误。
由于所有 javascript 运行时错误都是 Error 类型(或扩展类型,例如TypeError
),因此函数的实际类型将始终为type | Error
。
语法很简单,函数定义可以以 throws 子句结尾,后跟一个类型:
function fn() throws string { ... }
function fn(...) throws string | number { ... }
class MyError extends Error { ... }
function fn(...): Promise<string> throws MyError { ... }
捕获异常时,语法与声明错误类型的能力相同:
catch(e: string | Error) { ... }
例子:
function fn(num: number): void throws string {
if (num === 0) {
throw "error: can't deal with 0";
}
}
很明显,该函数可能会引发错误并且错误将是一个字符串,因此在调用此方法时,开发人员(和编译器/IDE)会意识到它并可以更好地处理它。
所以:
fn(0);
// or
try {
fn(0);
} catch (e: string) { ... }
编译没有错误,但是:
try {
fn(0);
} catch (e: number) { ... }
编译失败,因为number
不是string
。
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";
}
}
抛出string
。
function fn2(num: number) {
if (num < 0) {
throw new MyError("can only deal with positives");
}
fn(num);
}
抛出MyError | string
。
然而:
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);
}
}
}
仅抛出MyError
。
只是为了澄清 - 这里的一个想法不是强迫用户捕获异常,而是更好地推断 catch 子句变量的类型?
@DanielRosenwasser
是的,用户不会被迫捕获异常,所以这对编译器来说很好(在运行时当然会抛出错误):
function fn() {
throw "error";
}
fn();
// and
try {
fn();
} finally {
// do something here
}
但它将为开发人员提供一种方法来表达可以抛出哪些异常(在使用其他库.d.ts
文件时拥有这种方式会很棒),然后让编译器类型保护 catch 子句中的异常类型。
检查投掷与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);
}
代替
try {
const result: Result = mightFail();
console.log(success);
} catch (error: Error) {
console.error(error);
}
@aleksey-bykov
您建议不要在我的代码中使用throw
而是包装结果(在可能出错的函数中)。
这种方法有一些缺点:
Tried<>
的函数不能选择忽略错误。添加throws
将使选择处理来自其代码、第 3 个库和本机 js 的错误的开发人员能够。
由于该建议还要求进行错误推断,因此所有生成的定义文件都可以包含throws
子句。
知道函数可能直接从定义文件中抛出哪些错误而不是您需要转到文档的当前状态将非常方便,例如要知道JSON.parse
可能抛出的错误我需要去到MDN 页面并阅读:
如果要解析的字符串不是有效的 JSON,则抛出
SyntaxError
异常
当错误被记录时,这是一个很好的例子。
当错误被记录时,这是一个很好的例子。
javascript中是否有可靠的方法来区分SyntaxError和Error?
是的,它是更多的代码,但是由于在一个对象中表示了一个不好的情况,它可以像任何其他值一样被传递以进行处理、丢弃、存储或转换为有效结果
你也可以通过返回尝试来忽略尝试,尝试可以被视为一个单子,寻找单子计算
function mightFail(): Tried<number, string> {
}
function mightFailToo(): Tried<number, string> {
const tried = mightFail();
if (isSuccess(tried)) {
return successFrom(tried.result * 2);
} else {
return tried;
}
}
它对于您的代码来说已经足够标准了,当涉及到 3rd 方库抛出异常时,它通常意味着您的游戏结束,因为几乎不可能从异常中可靠地恢复,原因是它可以从代码中的任何地方抛出在任意位置终止它并使其内部状态不完整或损坏
JavaScript 运行时不支持检查异常,恐怕它不能单独在 typescript 中实现
除了将异常编码为特殊结果案例之外,这是 FP 世界中非常普遍的做法
而将可能的结果分为两部分:
看起来困难重重
在我看来,当你无能为力时, throw 有利于快速而响亮地失败,明确编码的结果适用于任何暗示你可以从中恢复的糟糕但预期的情况
考虑:
// 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
我对JSON.parse
的观点可能会抛出SyntaxError
是我需要在文档中查找该函数才能知道它可能会抛出,并且在.d.ts
中更容易看到
是的,你可以知道它是SyntaxError
使用instanceof
。
您可以通过抛出错误来表示同样的糟糕情况。
您可以创建自己的错误类来扩展Error
并将您需要的所有相关数据放入其中。
你用更少的代码得到同样的结果。
有时你有很长的函数调用链,你可能想要处理链中不同级别的一些错误。
总是使用包装的结果(单子)会很烦人。
更不用说,其他库和本机错误无论如何都可能被抛出,所以你最终可能会同时使用 monad 和 try/catch。
我不同意你的观点,在很多情况下,你可以从抛出的错误中恢复,如果语言能让你更好地表达它,那么这样做会更容易。
就像打字稿中的很多东西一样,javascript 中缺乏对该功能的支持不是问题。
这:
try {
mightFail();
} catch (e: MyError | string) {
if (e instanceof MyError) { ... }
else if (typeof e === "string") { ... }
else {}
}
将在 javascript 中按预期工作,只是没有类型注释。
使用throw
足以表达您的意思:如果操作成功返回值,否则抛出错误。
然后,此功能的用户将决定是否要处理可能的错误或忽略它们。
您只能处理自己抛出的错误,而忽略例如第 3 方的错误。
如果我们谈论浏览器instanceof
仅适用于源自同一窗口/文档的内容,请尝试:
var child = window.open('about:blank');
console.log(child.Error === window.Error);
所以当你这样做时:
try { child.doSomething(); } catch (e) { if (e instanceof SyntaxError) { } }
你不会抓住它
异常的另一个问题是它们可能会从您期望它们发生的地方滑入您的代码中
try {
doSomething(); // <-- uses 3rd party library that by coincidence throws SyntaxError too, but you don' t know it
} catch (e) {}
除了instanceof
容易受到原型继承的影响,因此您需要格外小心以始终检查最终祖先
class StandardError {}
class CustomError extends StandardError {
}
function doSomething() { throw new CustomError(); }
function oldCode() {
try {
doSomething();
} catch (e) {
if (e instanceof StandardError) {
// problem
}
}
}
@aleksey-bykov 正如您在 monadic 结构中建议的那样,显式地处理错误是一项非常艰巨且令人生畏的任务。 这需要付出很多努力,使代码难以理解,并且需要语言支持/类型驱动的 Emit 才能处于可以忍受的边缘。 这是一个为推广 Haskell 和 FP 作为一个整体而付出了很多努力的人的评论。
这是一个可行的选择,特别是对于爱好者(包括我自己)来说,但我认为这对于更多的观众来说不是一个可行的选择。
实际上,我主要担心的是人们会开始继承 Error。 我认为这是一个可怕的模式。 更一般地说,任何促进使用 instanceof 运算符的东西都会对类造成额外的混淆。
这是一个为推广 Haskell 和 FP 作为一个整体而付出了很多努力的人的评论。
我真的认为这应该更努力地向观众推送,直到它被消化并要求更多,我们才能在语言中获得更好的 FP 支持
它并不像你想象的那么令人生畏,只要所有组合器都已经编写好了,只需使用它们来构建数据流,就像我们在项目中所做的那样,但我同意 TS 可以更好地支持它:#2319
Monad 转换器是一个真正的 PITA。 您需要经常起重、起重和选择性运行。 最终结果是难以理解的代码,并且比所需的进入门槛高得多。 所有的组合器和提升功能(提供强制性装箱/拆箱)只是分散您手头问题的噪音。 我确实相信明确说明状态、效果等是一件好事,但我认为我们还没有找到方便的包装/抽象。 在我们找到它之前,支持传统的编程模式似乎是一条无需停下来进行实验和探索的方法。
PS:我认为我们需要的不仅仅是自定义运算符。 Higher Kinded Types 和某种类型的类对于实用的 monadic 库也是必不可少的。 其中,我将 HKT 评为第一,打字课程紧随其后。 尽管如此,我相信 TypeScript 不是实践这些概念的语言。 玩弄 - 是的,但它的理念和根源对于适当的无缝集成从根本上来说是遥远的。
回到 OP 问题 - instanceof
是一个危险的操作符。 但是,明确的例外不限于Error
。 您也可以抛出自己的 ADT 或自定义 POJO 错误。 提议的功能可能非常有用,当然,也可能被滥用。 无论如何,它使功能更加透明,这无疑是一件好事。 总的来说,我是 50/50 :)
@aleksey-bykov
开发人员应该意识到你描述的不同的 js 问题,毕竟在 typescript 中添加throws
并没有给 js 带来任何新的东西,它只是让 typescript 作为一种语言能够表达现有的 js 行为。
3rd 方库可以抛出错误的事实正是我的观点。
如果他们的定义文件要包含它,那么我将有办法知道它。
@aluanhaddad
为什么扩展Error
是一种糟糕的模式?
@gcnew
至于instanceof
,这只是一个例子,我总是可以抛出具有不同类型的常规对象,然后使用类型保护来区分它们。
这将由开发人员决定他希望抛出什么类型的错误,并且可能已经是这种情况,但目前没有办法表达这一点,这就是这个建议想要解决的问题。
@nitzantomer子类化原生类( Error
、 Array
、 RegExp
等)在较旧的 ECMAScript 版本(ES6 之前)中不受支持。 这些类的低级排放会产生意想不到的结果(已尽最大努力,但这是可以做到的),并且是每天记录大量问题的原因。 作为一个经验法则——除非你的目标是最近的 ECMAScript 版本并且真的知道你在做什么,否则不要子类化原生。
@gcnew
哦,我很清楚这一点,因为我花了几个多小时试图找出问题所在。
但是现在有了这样做的能力,应该没有理由不这样做(当针对 es6 时)。
无论如何,这个建议并不假定用户是 Error 类的子类,它只是一个例子。
@nitzantomer我并不是说该建议仅限于Error
。 我只是解释了为什么将其子类化是一种不好的模式。 在我的帖子中,我实际上捍卫了也可以使用自定义对象或有区别的工会的立场。
instanceof
是危险的并且被认为是一种反模式,即使你去掉了 JavaScript 的特殊性——例如当心 instanceof 操作符。 原因是编译器无法保护您免受新子类引入的错误的影响。 使用instanceof
的逻辑很脆弱,不遵循开放/封闭原则,因为它只需要少数选项。 即使添加了通配符,新的派生词仍然可能导致错误,因为它们可能会破坏撰写本文时所做的假设。
对于您想要区分已知替代方案的情况,TypeScript 具有标记联合(也称为可区分联合或代数数据类型)。 编译器确保处理所有情况,这为您提供了很好的保证。 不利的一面是,如果您想向该类型添加一个新条目,则必须遍历所有识别它的代码并处理新添加的选项。 好处是这样的代码很可能会被破坏,但会在运行时失败。
我只是重新考虑了这个建议并反对它。 原因是如果throws
声明出现在签名上但没有被强制执行,它们已经可以由文档注释处理。 在被强制执行的情况下,我认为它们会变得烦人并被迅速吞下,因为 JavaScript 缺乏 Java 的类型化 catch 子句机制。 使用异常(尤其是作为控制流)也从未成为一种既定做法。 所有这一切让我明白,检查异常带来的影响太少,而更好的和目前更常见的表示失败的方法是可用的(例如联合返回)。
@gcnew
这就是它在 C# 中的完成方式,问题是文档在 typescript 中不是标准的。
我不记得遇到过有据可查的定义文件。 不同的lib.d.ts
文件确实包含注释,但不包含抛出的错误(有一个例外: lib.es6.d.ts
$ 在Date[Symbol.toPrimitive](hint: string)
$ 中有一个throws
#$ )。
此外,此建议考虑了错误推断,如果错误来自文档注释,则不会发生这种情况。 对于推断的检查异常,开发人员甚至不需要指定throws
子句,编译器将自动推断它并将其用于编译并将其添加到生成的定义文件中。
我同意强制执行错误处理不是一件好事,但是拥有此功能只会添加更多信息,然后可供希望使用的人使用。
问题在于:
... 有更好的和目前更常见的方式来表示失败
是不是没有标准的做法。
您可能会使用 union return,@aleksey-bykov 将使用Tried<>
,而另一个 3rd 方库的开发人员将做一些完全不同的事情。
抛出错误是跨语言(js、java、c#...)的标准,因为它是系统的一部分而不是解决方法,它应该(在我看来)在 typescript 中具有更好的处理能力,证明这一点的数字随着时间的推移,我在这里看到的问题中要求在catch
子句中进行类型注释。
如果函数(或称为函数)可以抛出,我希望在 VS 的工具提示中提供信息。 对于*.d.ts
文件,从 TS2.0 开始,我们可能需要像这样的假参数。
@HolgerJeromin
为什么需要它?
这是一个简单的问题,在下面的代码中应该为dontCare
推断出什么签名?
function mightThrow(): void throws string {
if (Math.random() > 0.5) {
throw 'hey!';
}
}
function dontCare() {
return mightThrow();
}
根据你在提案中所说的应该是
function dontCare(): void throws string {
我说这应该是类型错误,因为未正确处理已检查的异常
function dontCare() { // <-- Checked exception wasn't handled.
^^^^^^^^^^
这是为什么?
因为否则很有可能使直接调用者的状态损坏:
class MyClass {
private values: number[] = [];
keepAllValues(values: number[]) {
for (let index = 0; index < values.length; index ++) {
this.values.push(values[index]);
mightThrow();
}
}
}
如果您让异常通过,则无法将其推断为已检查,因为这样会违反keepAllValues
的行为契约(尽管最初的意图并非所有值都被保留)
唯一安全的方法是立即抓住它们并明确地重新抛出它们
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;
}
}
}
否则,尽管调用者知道什么可以被攻击,但你不能向他们保证使用刚刚抛出的代码是安全的
所以没有自动检查异常合同传播之类的东西
如果我错了,请纠正我,这正是 Java 所做的,您之前作为示例提到过
@aleksey-bykov
这:
function mightThrow(): void {
if (Math.random() > 0.5) {
throw 'hey!';
}
}
function dontCare() {
return mightThrow();
}
意味着mightThrow
和dontCare
都被推断为throws string
,但是:
function dontCare() {
try {
return mightThrow();
} catch (e: string) {
// do something
}
}
不会有throw
子句,因为错误已被处理。
这:
function mightThrow(): void throws string | MyErrorType { ... }
function dontCare() {
try {
return mightThrow();
} catch (e: string | MyErrorType) {
if (typeof e === "string") {
// do something
} else { throw e }
}
}
将有throws MyErrorType
。
至于您的keepAllValues
示例,我不确定您的意思,在您的示例中:
class MyClass {
private values: number[] = [];
keepAllValues(values: number[]) {
for (let index = 0; index < values.length; index ++) {
this.values.push(values[index]);
mightThrow();
}
}
}
MyClass.keepAllValues
将被推断为throws string
因为mightThrow
可能会抛出string
并且该错误未被处理。
至于您的
keepAllValues
示例,我不确定您的意思
我的意思是mightThrow
中断keepAllValues
未处理的异常,并使其在它正在做的事情中完成,使其状态腐败。 这是一个问题。 你的建议是对这个问题闭上眼睛,假装它不严重。 我的建议是通过要求立即处理并明确重新抛出所有检查的异常来解决这个问题。 这样就不可能无意中让国家腐败。 尽管如果您选择这样做,它仍然可能会损坏,但它需要一些刻意的编码。
想一想,我们可以通过两种方式处理异常:
现在,如果我们决定使用经过适当处理并防止崩溃的检查异常,我们需要排除当我们处理来自您捕获它的多层深处的异常时的情况:
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
}
上面的例子带来了一个有趣的案例供考虑,推断的签名是什么:
function boom(value: number) /* what comes here?*/ {
return 1/value;
}
另一个有趣的案例
// 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
所以你建议必须像处理java一样处理所有错误?
我不喜欢它(即使我来自 java 并且仍然喜欢它)因为 js/ts 更加动态并且他们的用户已经习惯了。
如果在编译时包含它(如strictNullChecks
),它可能是一个让您处理错误的标志。
我的建议不是在这里解决未处理的异常,您发布的代码现在会在没有实现此功能的情况下中断,并且它也会在 js 中中断。
我的建议只是让您作为开发人员更加了解可能引发的不同错误,是否处理它们或忽略它们仍然取决于您。
至于除以 0 的问题,它不会导致错误:
console.log(1 / 0) // Infinity
console.log(1 / "hey!") // NaN
更了解可能引发的不同错误
除非他们能够处理,否则这样做是没有意义的,由于我列出的案例,目前的提议是不可行的
所以你建议必须像处理java一样处理所有错误?
是的,这就是检查异常的意思
@aleksey-bykov
我不明白为什么您列出的任何案例都使该提案不可行。
处理在调用链中抛出的错误没有问题,即使我使用的函数被推断为抛出DivisionByZero
(不管它被抛出的位置),我也可以选择处理它.
我可以尝试使用不同的参数重试它,我可以向用户显示出错的消息,我可以记录这个问题,以便我以后可以更改我的代码来处理它(如果它经常发生的话)。
同样,这个提议在运行时不会改变任何东西,所以所有的工作都会像以前一样继续工作。
唯一的区别是我将获得有关可能引发的错误的更多信息。
我明白你在说什么,在 javascript 运行时不会有任何改变,但是你在这里的信息是给用户一种错觉,即他们通过处理来自 20 层以下的异常以同样的信心知道自己在做什么他们会立即处理异常
他们根本无法解决发生在 20 层以下的问题
当然,您可以记录它,就像任何未经检查的异常一样,但您无法修复它
所以一般来说都是谎言,TS里的谎言已经够多了,大家不要再迷惑了
@aleksey-bykov
您所描述的内容存在于所有支持异常的语言中。
没有人说捕获异常可以解决问题,但它会让你优雅地处理它。
知道调用函数时会抛出哪些错误将有助于开发人员区分他们可以处理的错误和他们不能处理的错误。
现在开发人员可能不知道使用JSON.parse
可能会引发错误,但如果它是lib.d.ts
的一部分并且 IDE 会让他知道(例如)那么也许他会选择处理这个案子。
你不能优雅地处理 20 层以下发生的问题,因为 19 层的内部状态已损坏,你不能去那里,因为状态是私有的
具有建设性:我的建议是要求用户立即处理已检查的异常并明确地重新抛出它们,这样我们就可以排除意外的混淆并将已检查的异常与未检查的异常分开:
SyntaxError
JSON.parse
应声明为检查异常
@aleksey-bykov
我不明白为什么需要强制开发人员做他们不希望做的事情,他们迄今为止还没有做过的事情。
这是一个例子:
我有一个网络客户端,用户可以在其中写入/粘贴 json 数据,然后单击一个按钮。
该应用程序接受此输入并将其传递给第 3 方库,该库以某种方式解析此 json 并返回 json 以及不同类型的值(字符串、数字、布尔值、数组等)。
如果这个第 3 方库抛出SyntaxError
我可以恢复:通知用户他的输入无效,他应该再试一次。
通过了解在调用函数时可能会引发哪些错误,开发人员可以决定他可以/希望处理什么以及不可以处理什么。
抛出错误在链中的深度无关紧要。
看你好像没明白我在说什么,我们在兜圈子
通过让SyntaxError
从 3rd 方库中抛出,您将让您的用户接触到您自己的代码的实现细节,这些代码应该被封装
基本上你是在说,嘿,不是我的代码不起作用,而是我在互联网上找到并使用的那个愚蠢的库,所以如果你有问题,处理那个 3rd 方库,而不是我,我刚刚说了我被要求说的话
并且不能保证在 SyntaxError 之后您仍然可以使用该 3rd 库的实例,您有责任向用户提供保证,例如在它抛出后重新实例化第 3 方控件
底线,你需要负责处理内部异常(不是所有的,只有检查的,我求你了)
我明白你在说什么,但我不同意。
你是对的,这基本上就是我要说的。
如果我使用了引发错误的 3rd 方库,我可以选择处理它或忽略它,让我的代码的用户处理它。
这样做的原因有很多,例如我正在编写的 lib 与 UI 无关,所以我无法通知用户有问题,但是曾经使用过我的 lib 的人可以处理使用时抛出的错误我的库并通过与用户交互来处理它们。
如果一个库在抛出时处于损坏状态,那么它可能需要记录它。
如果我然后使用这样的库并导致其中出现错误,我的状态就会损坏,那么我需要记录它。
底线:
此建议旨在提供有关抛出错误的更多信息。
它不应该强制开发人员做不同的事情,只是让他们更容易处理错误,如果他们愿意的话。
你可以不同意,没关系,我们不要称它们为受检异常,因为你说的方式不是受检异常
让我们称它们为列出或显示的异常,因为您所关心的只是让开发人员意识到它们
@aleksey-bykov
说白了,改名了。
@aleksey-bykov
你不能优雅地处理 20 层以下发生的问题,因为 19 层的内部状态已损坏,你不能去那里,因为状态是私有的
不,你不能修复内部状态,但肯定可以修复本地状态,这正是在这里处理它的重点,而不是在堆栈更深的地方。
如果您的论点是在处理异常时无法确定某些共享可变值处于什么状态,那么这是反对命令式编程的论点,并且不限于此提议。
如果每一层都必须负责对来自下一层的异常立即做出反应,那么成功恢复的机会就会大得多,这就是我所看到的检查异常背后的想法
换句话说,来自下面超过 1 级的异常是一个句子,除了从头开始重新实例化所有基础设施之外,现在做任何事情都为时已晚(如果你足够幸运,没有你可以做的全局剩余部分)达不到)
如上所述的提议大多是无用的,因为没有可靠的方法来对你无法触及的坏事做出反应
这很棒。 FWIW:我认为如果添加,默认情况下应该要求处理 throwing 方法或将您的方法标记为 throwing。 否则,它几乎只是文档。
@agonzalezjr
我认为像打字稿中的大多数功能一样,您也应该能够选择加入此功能。
就像添加类型不是强制性的一样,抛出/捕获也不应该是必须的。
可能应该有一个标志让它成为必须的,比如--onlyCheckedExceptions
。
在任何情况下,此功能也将用于推断/验证抛出异常的类型,而不仅仅是用于文档。
@nitzantomer
这是一个例子:
我有一个网络客户端,用户可以在其中写入/粘贴 json 数据,然后单击一个按钮。
该应用程序接受此输入并将其传递给第 3 方库,该库以某种方式解析此 json 并返回 json 以及不同类型的值(字符串、数字、布尔值、数组等)。
如果这个第 3 方库抛出 SyntaxError 我可以恢复:通知用户他的输入无效,他应该再试一次。
这无疑是受检查异常的整个概念变得模糊的一个领域。 这也是_异常情况_的定义变得不清楚的地方。
您示例中的程序将是JSON.parse
被声明为抛出已检查异常的参数。
但是,如果程序是一个 HTTP 客户端,并且基于附加到 HTTP 响应的标头的值调用JSON.parse
,而该响应恰好包含格式不正确的正文,该怎么办? 程序无法恢复任何有意义的事情,它所能做的就是重新抛出。
我会说这是反对将JSON.parse
声明为已检查的论点。
这一切都取决于用例。
我了解您提议将其置于标志之下,但让我们假设我想使用此功能,因此我启用了标志。 根据我正在编写的程序类型,它可能会帮助我,也可能会阻碍我。
甚至经典的java.io.FileNotFoundException就是一个例子。 已检查但程序可以恢复吗? 这实际上取决于丢失的文件对调用者意味着什么,而不是被调用者。
@aluanhaddad
该建议不建议添加任何新功能,只是添加一种在 typescript 中表达 javascript 中已经存在的内容的方法。
抛出错误,但目前 typescript 无法声明它们(在抛出或捕获时)。
至于您的示例,通过捕获错误,程序可以通过捕获此错误“优雅地”失败(例如向用户显示“出现问题”消息),或者它可以忽略它,具体取决于程序/开发人员。
如果程序的状态会受到此错误的影响,那么处理它可以保持有效状态而不是损坏状态。
在任何情况下,开发人员都应该决定他是否可以从抛出的错误中恢复。
恢复的含义也由他来决定,例如,如果我正在编写这个 http 客户端以用作第 3 方库,我可能希望从我的库中抛出的所有错误都属于同一类型:
enum ErrorCode {
IllFormedJsonResponse,
...
}
...
{
code: ErrorCode;
message: string;
}
现在,在我的库中,当我使用JSON.parse
解析响应时,我想捕获一个抛出的错误,然后抛出我自己的错误:
{
code: ErrorCode.IllFormedJsonResponse,
message: "Failed parsing response"
}
如果实现了此功能,那么我将很容易声明此行为,并且我的库的用户将清楚它是如何工作和失败的。
该建议不建议添加任何新功能,只是添加一种在 typescript 中表达 javascript 中已经存在的内容的方法。
我知道。 我说的是 TypeScript 在这个提议下会发出的错误。
我的假设是,该提案暗示了已检查和未检查异常说明符(推断或显式)之间的区别,同样仅用于类型检查目的。
@aluanhaddad
你在之前的评论中说的:
但是,如果该程序是一个 HTTP 客户端,并且基于附加到恰好包含格式错误的正文的 HTTP 响应的标头的值来调用 JSON.parse,该怎么办? 程序无法恢复任何有意义的事情,它所能做的就是重新抛出。
当我的函数被声明为返回结果时,同样适用于返回null
。
如果开发人员选择使用strictNullChecks
,那么如果函数返回null
(而不是抛出),那么您可以说完全相同的事情,那么在相同的场景中“程序没有任何意义可以做恢复”。
但即使不使用onlyCheckedExceptions
标志,此功能仍然很有用,因为编译器会抱怨,例如,如果我在声明函数仅抛出Error
时尝试将错误捕获为string
Error
。
好主意,这会有所帮助,但不是严格/类型安全的,因为无法知道嵌套调用可能会给您带来什么。
意思是,如果我有一个可能抛出 A 类型异常的函数,但在内部我调用了一个嵌套函数并且不将它放入 try catch - 它会向我的调用者抛出它的 B 类型异常。
因此,如果调用者只期望 A 类异常,则不能保证他不会从嵌套异常中获取其他类型。
(线程太长了 - 如果我错过了这个评论,对不起)
@shaipetel
该命题指出编译器将推断未处理错误的类型并将它们添加到函数/方法签名中。
因此,在您描述的情况下,如果未处理B
,您的函数将抛出A | B
。
原来如此。 它将深入了解我调用的所有方法并收集所有可能的异常类型?
如果可能的话,我很想看到它发生。 看,开发人员总是有一个不会被声明的意外异常,在这种情况下,几乎任何函数都可能出现“对象未设置为实例”或“除以 0”或类似异常。
恕我直言,最好像在 C# 中那样处理它,其中所有异常都从具有消息的基类继承,并且根本不允许抛出未包装的文本或其他对象。 如果您有基类和继承,您可以级联您的捕获并在一个块中处理您预期的错误,而在另一个块中处理其他意外错误。
@shaipetel
在 javascript 中,所有错误都基于Error
类,但您不限于抛出错误,您可以抛出任何错误:
throw "something went wrong"
throw 0
throw { message: "something went wrong", code: 4 }
是的,我知道 JavaScript 是如何工作的,我们正在讨论 TypeScript,它会带来更多限制。
我建议,恕我直言,一个好的解决方案是让 TypeScript 遵循异常处理,该异常处理要求所有抛出的异常都属于特定的基类,并且不允许直接抛出未包装的值。
因此,它不允许“抛出 0”或“抛出‘一些错误’”。
就像 JavaScript 允许 TypeScript 不允许的许多事情一样。
谢谢,
@nitzantomer
当我的函数被声明为返回结果时,同样适用于返回 null。
如果开发人员选择使用 strictNullChecks,那么如果函数返回 null(而不是抛出),那么您可以说完全相同的事情,那么在相同的情况下“程序无法恢复任何有意义的事情”。
但即使不使用 onlyCheckedExceptions 标志,此功能仍然有用,因为编译器会抱怨,例如,当函数声明为仅抛出错误时,如果我尝试将错误作为字符串捕获。
我明白你说的,很有道理。
@shaipetel正如之前在本提案和其他地方所讨论的那样, Error
和Array
等内置函数的子类化不起作用。 它会导致在广泛使用的运行时和编译目标中出现不同的行为。
捕获各种非错误类型的值是两种设想所提议功能的最可行方式。 我实际上并不认为这是一个问题,因为利用此功能的异常传播机制可能会导致比堆栈跟踪或其他错误特定属性更具体、更有帮助的错误。
扩展Error
是不可行的。 在所有低于 es2015 的目标都不再使用之前,它是不可行的。
如果这个提议间接导致Error
函数的更多子类化,那么我认为这是一个坏主意。 这种反对意见与任何哲学上的反对意见完全分开,这些反对意见是围绕使用控制流的例外或例外情况的定义。 因此,如果这个提议被采纳,我会期待关于正确使用和避免子类化Error
的必要性的非常响亮和面对面的文档。
我希望有某种辅助逻辑来处理类型化异常,我正在重构很多承诺代码以使用 async/await,目前看起来像:
doSomethingWhichReturnsPromise()
.then(send(200))
.catch(NotFoundError, (error) => { send(404); })
.catch(SomeBadDataError, (error) => { send(400); })
.catch(CantSeeThisError, (error) => { send(403); })
.catch((error) => { send(500); })
然后在新世界中它看起来像这样:
{
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); }
}
这没问题,但需要更多代码,并且在某些方面可读性稍差,所以如果有某种形式的支持会很棒:
{
await doSomethingWhichReturnsPromise();
send(200);
}
catch(NotFoundError, error) { send(404); }
catch(SomeBadDataError, error) { send(404); }
catch(CantSeeThisError, error) { send(404); }
catch(error) { send(404); }
这将输出前一点,但作为语法糖,您甚至可以将其作为泛型来执行,但它有点讨厌:
{
await doSomethingWhichReturnsPromise();
send(200);
}
catch<NotFoundError>(error) { send(404); }
catch<SomeBadDataError>(error) { send(404); }
catch<CantSeeThisError>(error) { send(404); }
catch(error) { send(404); }
@grofit
如果打字稿支持你的建议,我会喜欢它,但我认为它与这个问题无关。
您的建议甚至可以在不实现此问题的情况下实现,只是编译器将无法抱怨(例如) NotFoundError
没有被抛出。
我认为这是对键入捕获的建议,而不是任何类型的问题,我只是不想复制线程,然后将其发布在自己的问题中。
@grofit
类型化的捕获也是功能请求的一部分,而且还能够通知编译器可以从函数中抛出什么类型的错误(然后编译器也能够推断出可以抛出的错误类型是什么)。
在我看来,类型化的 catch 可以在没有其他部分的情况下实现。 开个新issue,说不定TS团队会决定把它标记为重复,我不知道。
@grofit
这将输出前一点,但作为语法糖,您甚至可以将其作为泛型来执行,但它有点讨厌:
try { await doSomethingWhichReturnsPromise(); send(200); } catch<NotFoundError>(error) { send(404); } catch<SomeBadDataError>(error) { send(404); } catch<CantSeeThisError>(error) { send(404); } catch(error) { send(404); }
不会起作用,因为它意味着仅基于类型生成代码,而
doSomethingWhichReturnsPromise()
.then(send(200))
.catch(NotFoundError, (error) => { send(404); })
.catch(SomeBadDataError, (error) => { send(400); })
.catch(CantSeeThisError, (error) => { send(403); })
.catch((error) => { send(500); })
在每种情况下都通过 Error 函数,并且可能使用instanceof
实现。 像这样的代码是有毒的,因为它鼓励扩展Error
,这是一件非常糟糕的事情。
@nitzantomer我同意这是一个单独的问题。 OP 有捕获不扩展Error
类型的示例。
@aluanhaddad
对于@grofit的要求,您可以执行以下操作:
try {
// do something
} catch (isNotFoundError(error)) {
...
} catch (isSomeBadDataError(error)) {
...
} catch (error) {
...
}
isXXX(error)
是以下形式的类型保护:
function isXXX(error): error is XXX { ... }
@nitzantomer当然,但类型保护不是问题。 问题是
class MyError extends Error {
myErrorInfo: string;
}
这是有问题的,但已经在这里讨论过了。 我不想强调这一点,但许多大型和知名的代码库都因采用这种糟糕的做法而受到了负面影响。 扩展Error
是个坏主意。
@aluanhaddad
我知道这一点,这就是为什么我提供了一种方法来完成@grofit请求但不需要扩展Error
的原因。 开发人员仍然可以根据需要扩展Error
,但编译器将能够生成不需要使用instanceof
的 js 代码
@nitzantomer我知道您知道,但我没有意识到您对@grofit的建议听起来是个好主意。
我只是在打死马,因为我不喜欢处理那些想让我使用这种模式的 API。 无论如何,如果我把这个讨论带离主题,我很抱歉。
这个讨论有没有更多的想法? 我很想在打字稿中看到throws
子句。
@aluanhaddad你一直说扩展Error
是不好的做法。 你能详细说明一下吗?
已经详细讨论过了。 基本上你不能可靠地扩展内置类型。 该行为在环境和--target
设置的组合中显着变化。
我问的唯一原因是因为你的评论是我第一次听说这个。 谷歌搜索了一下,我只找到了解释如何以及为什么应该_应该_扩展Error
的文章。
不,它不会工作。 您可以在https://github.com/Microsoft/TypeScript/issues/12123和众多相关问题中详细了解它。
它会起作用,但只能在“特定环境”中工作,随着 ES6 的适应,这些环境正在成为主流。
@nitzantomer :你认为有可能实现一个TSLint检查,当一个带有 @throws 的函数在没有周围try/catch
的情况下被调用时通知开发人员?
@bennyn我是 TSLint 用户,但我从未考虑过制定新规则。
我真的不知道这是否可能(尽管我猜是这样),如果可以的话,这有多容易。
如果你试一试,请在这里更新,谢谢。
@shaipetel (还有@nitzantomer)
回覆:
是的,我知道 JavaScript 是如何工作的,我们正在讨论 TypeScript,它会带来更多限制。
我建议,恕我直言,一个好的解决方案是让 TypeScript 遵循异常处理,该异常处理要求所有抛出的异常都属于特定的基类,并且不允许直接抛出未包装的值。
因此,它不允许“抛出 0”或“抛出‘一些错误’”。
就像 JavaScript 允许 TypeScript 不允许的许多事情一样。
不确定这是否是您建议@shaipetel的内容,但以防万一……我会提醒您不要让 Typescript 将throw
限制为仅返回Error
s。 React 即将推出的异步渲染功能在throw
ing Promise
s 下工作! (我知道听起来很奇怪......但我读过他们已经评估了这个与async
/ await
和yield
/ yield*
生成器的用例我肯定知道他们在做什么!)
throw
ing Error
是我确信 99% 的用例,但不是 100%。 我认为 Typescript 不应该将用户限制在throw
扩展Error
基类的东西上。 它当然要先进得多,但是throw
除了错误/异常之外还有其他用例。
@mikeleonard
我同意,现有的 js 代码有很多例子,它们会抛出各种类型(我见过很多throw "error message string"
)。
新的 React 特性是另一个很好的例子。
只是我的两分钱,我正在将代码转换为异步/等待,所以我受到了抛出(顺便说一句,我讨厌异常)。 在我看来,在这个问题讨论时有一个 throws 子句会很好。 我认为即使允许“抛出任何”,它也会很有用。 (另外,可能是一个“nothrows”和编译器选项,默认为“nothrows”。)
允许键入函数抛出的内容似乎是一个自然的扩展。 可以选择在 TS 中输入返回值,对我来说,throw 只是另一种类型的返回(正如所有建议避免抛出的替代方案所证明的那样,例如 https://stackoverflow.com/a/39209039/162530)。
(就个人而言,我也希望有(可选)编译器选项来强制声明为 throws 的函数的任何调用者也必须声明为 throws 或捕获它们。)
我当前的用例:我不想将我的整个 [Angular] 代码库转换为使用异常进行错误处理(因为我讨厌它们)。 我在我的 API 的实现细节中使用 async/await,但是当 API 返回时将 throw 转换为正常的 Promises/Observables。 让编译器检查我是否捕捉到了正确的东西(或者完全捕捉到它们,理想情况下)会很好。
@aleksey-bykov 让我们不要改变 JavaScript 的本质,让我们只是为其添加类型。 :)
添加类型已经改变了它(删除了没有意义的代码),
同样我们可以加强异常处理
2018 年 7 月 26 日星期四晚上 8:22,Joe Pea [email protected]写道:
@aleksey-bykov https://github.com/aleksey-bykov我们不要改变
JavaScript 的本质,让我们只为它添加类型。 :)—
你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/13219#issuecomment-408274156 ,
或使线程静音
https://github.com/notifications/unsubscribe-auth/AA5PzfUNS5E093Z74WA4WCUaTyWRRZC3ks5uKl1FgaJpZM4LXwLC
.
对于 Promise,如果您使用then
链接而不是 async/await 并避免使用throw
,实际上这一切都非常漂亮。 这是概念草图的证明:
https://bit.ly/2NQZD8i - 游乐场链接
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)
})
谓词 catch 从联合中删除类型,同时返回一个reject()
添加到它们。 唯一不起作用的是联合参数类型的推断(可能是一个错误)
上例中解决的问题:
我看到对所有代码使用这个的主要问题是回调签名,为了使其以兼容的方式工作,默认的“返回类型”将是“可能抛出任何”,如果你想限制你会说throws X
或throws never
如果不是,例如
declare function pureMap<T>(t:T[], f: (x:T) => U throws never):U[] throws never
没有签名的版本:
declare function dirtyMap<T>(t:T[], f: (x:T) => U):U[]
实际上会默认为
declare function dirtyMap<T>(t:T[], f: (x:T) => U throws any):U[] throws any
以保证所有当前代码都能编译。
与strictNullChecks
不同,null 返回值很少见,我认为 JS 中的异常非常普遍。 在.d.ts
文件中对它们进行建模可能还不错(从依赖项中导入类型以描述您的错误),但这肯定是一项不平凡的工作,并且会产生巨大的联合。
我认为一个好的中间立场是关注 Promises 和async/await
,因为 Promise 已经是一个包装器,而异步代码是典型场景中错误处理分支最多的地方。 其他错误将是“未检查”
@spion-h4 这个(已经/将要)可以在打字稿上使用吗?
declare function dirtyMap<T>(t:T[], f: (x:T) => U throws any):U[] throws any
@bluelovers
不,打字稿目前不支持throws
关键字,这就是这个问题的全部内容。
需要注意的一点(不一定会阻止这个提议)是没有名义类,键入内置错误仍然不是特别有用,因为它们在结构上都是相同的。
例如:
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
这个问题与提案无关。
这样做你会遇到同样的问题:
class BaseClass {
propA!: string;
}
class MyClass1 extends BaseClass { }
class MyClass2 extends BaseClass { }
function fn(): MyClass1 {
return new MyClass2();
}
这是一个影响很多用例的语言限制。
@ahejlsberg对类型异常有很好的阅读: https ://www.artima.com/intv/handcuffs.html
我相信 TypeScript 可以很好地避免这些问题。 TypeScript 是关于 _real-world_ 问题的 _pragmatic_ 解决方案。 以我的经验,在大型 JavaScript 和 TypeScript 代码库中,错误处理是最大的问题之一——查看哪些错误函数_可能_抛出并且我_可能_想要处理是非常困难的。 只有通过阅读和编写好的文档(我们都知道我们在这个/s方面有多好)或查看实现才有可能,并且由于与返回值相反,异常只是通过函数调用自动传播,仅仅检查直接调用的函数,但要全部捕获它们,您还必须检查嵌套函数调用。 _这是一个现实世界的问题_。
JavaScript 中的错误实际上是非常有用的值。 NodeJS 中的许多 API 会抛出详细的错误对象,这些对象具有明确定义的错误代码并公开有用的元数据。 例如,来自child_process.execFile()
的错误具有类似exitCode
和stderr
的属性,来自fs.readFile()
的错误具有类似ENOENT
的错误代码(找不到文件) 或EPERM
(权限不足)。 我知道许多库也这样做,例如pg
数据库驱动程序为您提供了足够的错误元数据,以了解哪个确切的列约束导致INSERT
失败。
你可以看到代码库中错误消息的脆弱正则表达式检查数量令人担忧,因为人们不知道错误有正确的错误代码以及它们是什么。
如果我们可以在@types
声明和lib.d.ts
中定义这些函数可能引发的错误,TypeScript 将有能力帮助我们了解错误的结构 - 可能存在哪些错误,可能是什么错误代码值是,它们具有什么属性。 这不是关于类型化异常的,因此完全避免了类型化异常的所有问题。 它是对 _real-world 问题_ 的 _pragmatic 解决方案_(与告诉人们使用单子错误返回值相反 - JavaScript 领域的现实看起来不同,函数会抛出错误)。
注释可以是完全可选的(或者需要编译器标志)。 如果未在声明文件中指定,函数会简单地抛出any
(或unknown
)。
一个函数可以手动声明从不抛出throws never
。
如果实现可用,则函数抛出它调用的函数的所有异常类型及其自己的throw
语句的联合,这些语句不在带有catch
的try
块内catch
子句。
如果其中一个抛出any
,该函数也会抛出any
- 这没关系,在每个函数边界,开发人员都有机会通过显式注释来纠正它。
在许多情况下,当调用单个众所周知的函数并将其包装在 try/catch 中(例如,读取文件并处理未找到的文件)时,TypeScript 可以推断 catch 子句中的类型。
我们不需要为此进行 Error 子类化,也不需要instanceof
- 错误类型可以是接口,用字符串文字指定错误代码,TypeScript 可以区分错误代码上的联合,或使用类型保护。
定义错误类型
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 })
}
}
除了错误,是否也可以标记其他类型的副作用,如
Math.random
)它让我想起了来自 MSR 的Koka ,它可以标记返回类型的效果。
提案:
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
我喜欢声明错误类型的想法! 这对需要它的人有很大帮助,不会对不喜欢它的人做任何坏事。 我现在在我的节点项目中遇到了问题,使用此功能可以更快地解决这个问题。 我必须捕获错误并发回正确的 http 代码 - 为此我必须始终检查所有函数调用以找出我必须处理的异常 - 这并不好玩 -_-
PS。 https://github.com/Microsoft/TypeScript/issues/13219#issuecomment -428696412 中的语法看起来非常好 - &!
并将错误类型包装在<>
中真是太棒了。
我也赞成这一点。 现在所有的错误都是无类型的,这很烦人。 尽管我希望大多数throws
可以从代码中派生出来,但我们必须在任何地方编写它。
通过这种方式,人们甚至可以编写 tslint 规则,强制开发人员在其他端点等上捕获用户不友好的错误。
实际上,我很惊讶这个功能_已经_不在 TypeScript 中。 我去申报的第一件事。 编译器强制执行或不强制执行都可以,只要你能得到会抛出错误的信息,以及会抛出什么类型的错误。
即使我们没有得到&!
而只是得到Throws<T>
那将是 :+1:
这正是我的想法,如果 TypeScript 支持这些子句,那就更好了。
另一个可能的功能是以某种方式强制(可能在函数级别或模块级别)strictExceptionTracking - 这意味着您必须使用表达式try! invokeSomething()
调用声明为throws X
的任何内容就像在 Rust 和 swift 中一样。
它会简单地编译为invokeSomething()
,但异常的可能性将在代码中可见,如果您将某些可变的东西置于不良的瞬态状态(或未处理分配的资源),则更容易发现
@be5invis
function square(x: number): number &! never { // This function is pure return x * x } ...
我只是在这里给我两分钱,但&!
对我来说看起来非常难看。
我认为不熟悉打字稿的人会被这个符号吓跑,这真的很不受欢迎。
在我看来,一个简单的throws
更加明确、直观和简单。
除此之外,+1 用于键入的异常。 👍
我想为键入和检查的异常添加我的 +1。
我在代码中充分利用了类型化异常。 我完全意识到instanceof
的缺陷,实际上我已经利用它来为从公共基类继承的相关错误编写通用处理程序来发挥我的优势。 我遇到的大多数其他避免从基 Error 类继承的方法最终都(至少)以不同的方式同样复杂和有问题。
在我看来,检查异常是一种改进,有助于对代码库进行更深入的静态分析。 在足够复杂的代码库中,可能很容易错过特定函数引发的异常。
对于那些在 typescript 中寻找编译时错误安全的人,你可以使用我的ts-results库。
@vultix对我来说不幸的是,分离不够明确,无法将其放入团队设置中。
@vultix上面已经讨论了您的库的方法,这与此功能要解决的问题完全相反。
Javascript 已经有了处理错误的机制,它被称为异常,但是 typescript 缺少描述它的方法。
@nitzantomer我完全同意这个功能是打字稿的必要条件。 在添加此功能之前,我的图书馆只不过是临时替身。
function* range(min: number, max: number): Iterable<number> throws TypeError, RangeError {
我知道我在这个游戏上有点晚了,但下面似乎更多的是 Typescripty 语法而不是逗号。
Iterable<number> throws TypeError | RangeError
但我仍然擅长逗号。我只是希望我们在语言中有这个。
主要的是我想在JSON.parse()
上使用它,因为我的很多同事似乎忘记了JSON.parse
可能会引发错误,它会节省很多来回拉取请求。
@WORMSS
我完全同意。
我的建议包括您推荐的语法:
function mightThrow(): void throws string | MyErrorType { ... }
我们可以遵循像 Java 这样的 OOP 语言的错误声明模式吗? “throws”关键字用于声明我们在使用可能出现错误的函数时需要“try..catch”
@allicanseenow
该提案用于可选地使用 throws 子句。
也就是说,如果您使用抛出的函数,则不必使用 try/catch。
@allicanseenow你可能想阅读我上面的文章以了解上下文,包括链接的文章: https ://github.com/microsoft/TypeScript/issues/13219#issuecomment -416001890
我认为这是打字稿类型系统中缺少的东西之一。
它纯粹是可选的,不会更改发出的代码,有助于使用库和本机函数。
还知道可能会抛出什么错误可以允许编辑器使用 if 条件“自动完成”catch 子句来处理所有错误。
我的意思是,即使是很小的应用程序也应该得到适当的错误处理——现在最糟糕的是,用户不知道什么时候可能会出错。
对我来说,这是现在最缺少的功能。
@RyanCavanaugh ,自从添加了“等待更多反馈”标签以来,已经收到了很多反馈。 任何打字稿团队成员都可以参与吗?
@nitzantomer我支持将 throws 添加到 TypeScript 的想法,但是在嵌套函数中使用时,可选的 try/catch 不会允许潜在不准确的 throws 声明吗?
为了让开发人员相信方法的 throws 子句是准确的,他们必须做出危险的假设,即在该方法的调用层次结构中,任何时候都不会忽略可选异常并抛出未报告的堆栈。 我认为@ajxs可能在他的评论末尾提到了这一点,但我认为这将是一个大问题。 尤其是大多数 npm 库的碎片化程度。
@ConnorSinnott
我希望我理解你的正确:
编译器将推断抛出的类型,例如:
function fn() {
if (something) {
throw "something happened";
}
}
实际上是function fn(): throws string { ... }
。
当声明的错误与实际错误不匹配时,编译器也会出错:
function fn1() throws string | MyError {
...
}
function fn2() throws string {
fn1(); // throws but not catched
if (something) {
throws "damn!";
}
}
编译器应该抱怨fn2
抛出string | MyError
而不是string
。
考虑到所有这些,我不认为这种假设比开发人员在使用其他库、框架等时信任的其他声明类型的假设更危险。
我不确定我是否真的涵盖了所有选项,如果你能想出一个有趣的场景,我会很高兴。
即使有这个问题,它也和现在差不多:
// 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) { ... }
}
}
我们甚至可以让 typescript 保护我们,但它需要一种与 javascript 有点不同的语法:
function fn2() {
try {
fn1();
} catch(typeof e === "string") {
...
} catch(e instanceof MyError) {
...
}
}
这将被编译为:
function fn2() {
try {
fn1();
} catch(e) {
if (typeof e === "string") {}
else if (e instanceof MyError) {}
else {
throw e;
}
}
}
嘿! 感谢您的回复! 实际上,正如你提到的那样推断出投掷可以缓解我正在考虑的这个问题。 但是你列出的第二个例子很有趣。
给定
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;
}
为了显式声明 MyVeryImportantError,我还必须显式声明调用堆栈的所有其他错误,根据应用程序的深度,这些错误可能很少。 此外,可能很乏味。 我当然不想深入整个调用链来生成可能在途中出现的潜在错误列表,但也许 IDE 可以提供帮助。
我正在考虑提出某种扩展运算符,以允许开发人员明确声明他们的错误并将其余部分扔掉。
function first() : string throws MyVeryImportantError | ... { // Spread the rest of the errors
但是有一种更简单的方法可以实现相同的结果:只需删除 throws 声明。
function first() : string { // Everything automatically inferred
这就提出了一个问题:我什么时候会看到使用throws
关键字而不是让打字稿推断错误的好处?
@ConnorSinnott
它与使用 throw 子句的其他语言没有什么不同,即 java。
但我认为在大多数情况下,您实际上不必处理很多类型。 在现实世界中,您通常只会处理string
、 Error
(和子类)和不同的对象( {}
)。
在大多数情况下,您只能使用捕获多种类型的父类:
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();
}
至于何时隐式使用 throw 子句与让编译器推断它,我认为这就像在 typescript 中声明类型一样,在很多情况下您不必这样做,但您可以这样做以更好地记录您的代码,以便以后读的人能更好地理解它。
如果您想要类型安全,另一种方法是将错误视为 Golang 的值:
https://gist.github.com/brandonkal/06c4a9c630369979c6038fa363ec6c83
不过,这将是一个不错的功能。
@brandonkal
这种方法已经在线程的前面讨论过。
这是可能的,但这样做会忽略 javascript 允许我们使用的固有部分/功能。
我也认为noexcept
说明符(#36075)是目前最简单和最好的解决方案,对于大多数程序员来说,抛出异常被认为是一种反模式。
这里有一些很酷的功能:
noexcept function foo(): void {
throw new Error('Some exception'); // <- compile error!
}
try..catch
块的警告:try { // <- warning!
foo();
} catch (e) {}
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
对于大多数程序员来说,抛出异常是一种反模式。
我想我不是那个“大多数程序员”组的一部分。
noexcept
很酷,但这不是这个问题的意义所在。
如果工具在那里,我会使用它们。 throw
、 try/catch
和reject
都在那里,所以我会使用它们。
让他们在打字稿中正确输入会很好。
如果工具在那里,我会使用它们。
throw
、try/catch
和reject
都在那里,所以我会使用它们。
当然。 我也会使用它们。 我的观点是, noexcept
或throws never
将是这个问题的良好开端。
愚蠢的例子:我想知道如果我调用Math.sqrt(-2)
它永远不会抛出任何错误。
同样适用于第三方库。 它可以更好地控制我作为程序员需要处理的代码和边缘情况。
@moshest
但为什么?
这个提案包含了“noexpect”提案的所有好处,那么为什么要满足于更少呢?
因为它需要很长时间(这个问题是 3 岁),我希望我们至少可以先从最基本的功能开始。
@moshest
好吧,我更喜欢一个更好的功能,它比部分解决方案花费的时间更长,但我们会永远坚持下去。
我觉得noexcept
可以很容易地在以后添加,实际上它可以是一个单独的主题。
noexcept
与throws never
相同。
TBH 我认为拥有一些机制来保证处理异常(在 Go 中未使用error
)而不是为try/catch
提供类型提示更重要
@roll不应该有任何“保证”。
在javascript中处理异常不是必须的,在打字稿中也不应该是必须的。
作为 typecrypt 严格模式的一部分,函数必须捕获错误或显式声明它throws
并传递它,这可能是一个选项
添加我的 5 美分:我看到类似于返回null
的函数的异常:这是您通常不希望发生的事情。 但是如果它发生了,就会发生运行时错误,这很糟糕。 现在,TS 添加了“不可为空的类型”来提醒您处理null
。 我认为添加throws
会提醒您处理异常,从而使这些努力更进一步。 这就是为什么我认为这个功能是绝对需要的。
如果您查看 Go,它返回错误而不是抛出错误,您会更清楚地看到这两个概念并没有那么不同。 此外,它还可以帮助您更深入地了解一些 API。 也许您不知道某些函数会抛出异常,并且您会在生产后期注意到它(例如JSON.parse
,谁知道还有多少?)。
@obedm503这实际上是 TS 的哲学:默认情况下,编译器不会抱怨任何事情。 您可以启用选项以将某些事情视为错误或启用严格模式以一次启用所有选项。 所以,这应该是给定的。
我喜欢这个建议的功能。
异常类型和推理可能是整个编程历史中最好的事情之一。
❤️
您好,我刚刚花了一点时间试图找到解决我们目前在 TS 中所做的工作的方法。 由于无法获取函数范围内抛出的错误的类型(也无法获取当前方法的类型),因此我想出了如何在方法本身内显式设置预期错误。 我们稍后可以检索这些类型,并且至少知道可以在方法中抛出什么。
这是我的 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)
非常感谢您的积极反馈! 我意识到重构现有代码可能更容易,而不必将整个返回的类型包装到throwable
类型中。 相反,我们可以将其附加到返回的类型,以下代码允许您附加可抛出的错误,如下所示:
// now only append '& throwable<ErrorsThrown>' to the returned type
function unreliableNumberGenerator(): number & throwable<Error | CustomError> { /* code */ }
这是示例部分的唯一更改,这是新的类型声明:
/***********************************************
** 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>;
};
我还添加了一个新类型exceptionsOf
,它允许提取函数的错误以升级责任。 例如:
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;
}
由于exceptionsOf
得到一个错误并集,您可以根据需要升级任意数量的关键方法:
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();
}
我不喜欢使用typeof
,如果我找到更好的方法,我会告诉你
您可以在此处测试结果提示:将typedError
悬停在第 106 行
@Xample使用我们现在拥有的工具,这是一个很好的解决方案!
但我认为在实践中这还不够,因为您仍然可以执行以下操作:
const a = superRiskyMethod();
const b = a + 1;
并且 b 的类型被推断为正确的数字,但只要它包含在 try 中
这不应该是有效的
@luisgurmendezMLabs我不太明白你的意思。 如果您在第 56 行关注@Xample的repo 。您可以看到myNumber + 23
的结果被推断为number
,而myNumber: number & extraType<Error | CustomError>
。
在另一个阶段,您的示例中的a
永远不会包含在 Try Monad 之类的东西中。 除了与& extraType<Error | CustomError>
的交集之外,它根本没有包装器。
祝贺@Xample 的出色设计👏👏👏👏👏👏 。 这确实很有希望,即使没有任何语法糖也已经很有用了。 你有为此建立一个类型库的计划吗?
@ivawzh我的想法是,在实践中这可能会带来一些问题,因为:
function stillARiskyMethod() {
const a = superRiskyMethod();
return a + 1
}
该函数类型返回被推断为数字,这并不完全正确
@luisgurmendezMLabs throwable 类型在函数的返回类型中,仅作为将错误类型与函数结合的一种方式(并且能够在以后恢复这些类型)。 我从不真正返回错误,我只会抛出它们。 因此,无论您是否在 try 块内,都不会改变任何内容。
@ivawzh谢谢你的提问,我只是为你做的
@luisgurmendezMLabs啊好的我明白你的意思,看来打字稿只能推断找到的第一种类型。 例如,如果您有:
function stillARiskyMethod() {
return superRiskyMethod();
}
StillARiskyMethod 的返回类型将被正确推断,而
function stillARiskyMethod() {
return Math.random() < 0.5 superRiskyMethod() : anotherSuperRiskyMethod();
}
只会将返回类型键入为superRiskyMethod()
并关闭anotherSuperRiskyMethod()
之一。 我没有调查更多
因此,您需要手动升级错误类型。
function stillARiskyMethod(): number & throwable<exceptionOf<typeof superRiskyMethod>> {
const a = superRiskyMethod();
return a + 1
}
我也只是想谈谈我的想法。
我认为这将是一个非常好的实现功能,因为想要返回错误/空值和想要抛出一些东西有不同的用例,它可能有很大的潜力。 无论如何,抛出异常是 Javascript 语言的一部分,那么为什么我们不应该选择输入和推断这些异常呢?
例如,如果我正在执行许多可能出错的任务,我会发现每次都必须使用 if 语句来检查返回类型很不方便,而这可以通过使用 try/catch where if any其中一项任务抛出,它将在 catch 中处理,无需任何额外代码。
当您要以相同的方式处理错误时,这特别有用; 例如在 express/node.js 中,我可能想将错误传递给NextFunction
(错误处理程序)。
而不是每次都执行if (result instanceof Error) { next(result); }
,我可以将这些任务的所有代码包装在try/catch中,并且在我的catch中我知道,因为抛出了异常,我总是想将它传递给我的错误处理程序, catch(error) { next(error); }
也可以
还没有看到这个讨论过(可能已经错过了它,但是这个线程有很多评论!)但是如果实现了,是否必须强制(即:编译错误)有一个不使用抛出的函数throws
子句在其函数声明中? 我觉得这样做很好(我们不是强迫人们处理抛出,它只会通知他们该函数确实抛出)但这里最大的问题是,如果以这种方式更新 Typescript,它可能打破许多现有的代码。
编辑:我想到的另一个用例可能是这也有助于使用@throws 标签生成JSDocs
我将在这里重复我在现在提交的问题中所说的话。
我认为打字稿应该能够推断出大多数 JavaScript 表达式的错误类型。 允许库创建者更快地实施。
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();
}
a
我们知道错误类型是'unlucky'
,如果我们想非常谨慎,我们可以将其扩展为Error | 'unlucky'
,因此includeError
。b
将继承函数a
的错误类型。c
几乎相同; 'unlucky' | 'fairly lucky'
或Error | 'unlucky' | 'fairly lucky'
。d
将不得不抛出unknown
,因为 eval 是......未知e
捕获d
的错误,但是由于在 catch 块中有throw
,我们在这里推断它的类型'too bad...'
,因为只有块包含throw 'primitive value'
我们可以说它不能抛出Error
(如果我错过了一些 JS 黑魔法,请纠正我......)f
继承自c
与b
$ 继承自 $#$ a
$#$ 相同。g
$ 从a
$ 继承'unlucky'
并从 c 继承 $# unknown
c
因此'unlucky' | unknown
=> unknown
以下是我认为应该包括的一些编译器选项,因为用户使用此功能可能会根据他们的技能以及他们在给定项目中依赖的库的类型安全性而有所不同:
{
"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
}
}
}
至于如何表达任何函数可能产生的错误的语法,我不确定,但我知道我们需要它具有通用性和可推断性的能力。
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;
我的用例,因为显示实际用例可能会显示其他它具有值:
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!');
}
}
});
我需要一个错误接收器来防止针对多个错误多次发送响应标头(它们通常不会发生,但是当它们发生时,它们会批量发送!)
这个问题仍然被标记Awaiting More Feedback
,我们可以做些什么来提供更多反馈? 这是我羡慕java语言的唯一功能
我们有一个用例,当 API 调用返回非 200 代码时,我们会抛出特定错误:
interface HttpError extends Error {
response: Response
}
try {
loadData()
} catch (error: Error | ResponseError) {
if (error.response) {
checkStatusCodeAndDoSomethingElse()
} else {
doGenericErrorHandling()
}
}
无法键入 catch 块最终会导致开发人员忘记可以抛出两种可能类型的错误,并且他们需要同时处理这两种错误。
我更喜欢总是抛出错误对象:
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')) { .....}
}
}
为什么这个功能还是“反馈不够”? 在调用浏览器的 API(如 indexedDB、localstorage)时非常有用。 它在复杂的场景中导致了许多失败,但开发人员在编程中无法意识到。
黑格尔似乎完美地具备了这一特点。
https://hegel.js.org/docs#benefits (滚动到“类型错误”部分)
我希望 TypeScript 有类似的功能!
DL;博士;
try
块中抛出的任何类型都应该使用联合推断到catch
的错误参数中any
以防止错过抛出的错误。好吧,也许我们应该简单地澄清一下我们的需求和期望是什么:
js中错误的处理通常有3种方式
使用回调(实际上可以输入)
使用示例:
import * as fs from 'fs';
fs.readFile('readme.txt', 'utf8',(error, data) => {
if (error){
console.error(error);
}
if (data){
console.log(data)
}
});
fs.d.ts
给我们的地方:
function readFile(path: PathLike | number, options: { encoding: string; flag?: string; } | string, callback: (err: NodeJS.ErrnoException | null, data: string) => void): void;
因此错误是这样输入的
interface ErrnoException extends Error {
errno?: number;
code?: string;
path?: string;
syscall?: string;
stack?: string;
}
在承诺解决或拒绝的情况下,虽然您可以输入已解决的value
,但您不能输入通常称为reason
的拒绝。
这是一个 promise 的构造函数的签名:注意原因是any
new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;
理论上可以按如下方式键入它们:
new <T, U=any>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: U) => void) => void): Promise<T>;
通过这种方式,理论上我们仍然可以在节点回调和 Promise 之间进行 1-1 转换,从而保持所有输入过程。 例如:
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>;
try {
throw new Error();
}
catch (error: unknown) { //typing the error is possible since ts 4.0
if (error instanceof Error) {
error.message;
}
}
尽管我们在 catch 块前 2 行抛出了错误“Error”,但 TS 无法将error
(值)键入为Error
(类型)。
在async
函数中使用 promise 不是这样吗(“还没有”魔法)。 使用我们承诺的节点回调:
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);
}
}
打字稿没有丢失任何信息,可以预测在 try 范围内可能会抛出什么类型的错误。
然而,我们应该考虑内部的、本机函数可能会引发不在源代码中的错误,但是如果每次源代码中都有一个“throw”关键字,TS 应该收集类型并建议它作为错误的可能类型。 这些类型当然会受到try
块的限制。
这只是第一步,还有改进的余地,例如强制 TS 像在 Java 中一样工作的严格模式,即强制用户在try
内使用有风险的方法(可以抛出某些东西的方法) function example throw ErrorType { ... }
以提升处理错误的责任。
可以抛出任何东西,不仅是错误,甚至是对象的实例。 表示以下内容有效
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")
}
}
要知道错误可能是number | Error
类型,这将非常有帮助。 但是,为了防止忘记处理可能的错误类型,在没有严格的可能结果集的情况下使用单独的 if / else 块并不是最好的主意。
然而,一个 switch case 会做得更好,因为如果我们忘记匹配一个特定的 case(哪个会回退到 default 子句),我们会收到警告。 我们不能(除非我们做一些骇人听闻的事情)切换对象实例类型,即使我们可以,我们也可以抛出任何东西(不仅是一个对象,还有一个布尔值、一个字符串和一个没有“实例”的数字)。 但是,我们可以使用实例的构造函数来找出它是哪种类型。 我们现在可以重写上面的代码如下:
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;
}
}
霍雷……唯一剩下的问题是 TS没有键入error.constructor
,因此没有办法缩小 switch 大小写(但是?),如果这样做,我们将有一个安全的类型错误语言js。
如果您需要更多反馈,请发表评论
最有用的评论
@aleksey-bykov
您建议不要在我的代码中使用
throw
而是包装结果(在可能出错的函数中)。这种方法有一些缺点:
Tried<>
的函数不能选择忽略错误。添加
throws
将使选择处理来自其代码、第 3 个库和本机 js 的错误的开发人员能够。由于该建议还要求进行错误推断,因此所有生成的定义文件都可以包含
throws
子句。知道函数可能直接从定义文件中抛出哪些错误而不是您需要转到文档的当前状态将非常方便,例如要知道
JSON.parse
可能抛出的错误我需要去到MDN 页面并阅读:当错误被记录时,这是一个很好的例子。