TypeScript: Can't narrow error type in catch clause
try {
...
}
catch (ex) { // Adding an annotation here gives ERROR TS1196: Catch clause variable cannot have a type annotation
if (ex instanceof FooError) {
ex; // ex is of type any
ex.message; // OK
ex.iDontExist; // typechecks OK, but doesn't exist
}
else if (isBarError(ex)) {
ex; // ex is of type any
ex.foo(); // typechecks OK, but runtime error
}
else {
...
}
}
I guess this is expected behaviour not a bug. But I was not previously aware that the catch clasue variable cannot be annotated. Due to #1426, this means we can’t narrow ex inside the catch clause, so all the cases dealing with specific error types get no type checking (since ex remains as any).
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Reactions: 1
- Comments: 24 (19 by maintainers)
Forgive me, if I this has been discussed before, but whats wrong with doing something like this:
Which would generate JS like this:
If there is no catch all block, then just re-throw the exception:
JS:
And in the type annotations would maybe only accept constructors? So what’s “instanceof-able” in JS? Maybe also
string,number, and whatever is easily type-checkable withtypeof?Two interrelated issues being discussed here:
throwsannotations it is not clear that we could ever really rely on them. (And, it is also not clear that they work all that well in Java, but that’s another discussion.)anyin type guards unless we know the _exact_ type you’re narrowing to. For example, if you checktypeof x === "string"we will indeed narrow ananytostringbecause we know for sure that is the exact type ofx. That isn’t the case withinstanceof. Specifically, you might checkx instanceof Base, butxmight actually be an instance of a class derived fromBase. If we narrowedxto typeBaseyou’d see errors on access to properties of the derived class even though there is nothing wrong with the code. Now, before you argue those errors would be fine, keep in mind that the declared type ofxisany, so you’ve already said you want no checking. We take that as an indication that we should issue errors only when we’re 100% sure they’re errors. And we only know that if we know the exact type.@mcclure Personally I don’t think your suggestions are ‘unreliable’ any more or less that other patterns in TypeScript which can also be made unreliable if we try hard enough.
The thing that makes @bali182’s original suggestion likely incompatible with TypeScript’s design goals is that the following three examples only differ by a type annotation, but all would generate different JS code with different runtime behaviour:
Type annotations are there to inform the compiler about what the program actually does, but they never change what it does. With these ‘hints’, the type checker can detect invalid usage at compile-time and produce useful early feedback to the developer. If you strip out all the type annotations in a program, that would not change the meaning/behaviour of the program. But it would in the above example.
Your other suggestion (multiple
catch (e instanceof X) {...}blocks) is better in the sense that it doesn’t rely on type annotations. But it’s still a big ask, because it introduces new syntax that has no corresponding ECMA proposal (IFAIK). Apart from adding types to JavaScript, TypeScript has pioneered a few other kinds of new syntax (class properties, modules, namespaces, async/await), but generally only ones that either have an ECMA proposal in development, or represent a very widespread idiom (eg namespaces are just JavaScript’s ubiquitous ‘revealing module pattern’).But mainly, the problems in this issue would simply vanish if TypeScript just allowed the
catchvariable to be narrowed. We already have type guards and control flow analysis. I think there’s little need to add new syntax for this one special case, when everything is already provided to write idiomatic code, if only the catch variable could be narrowed:Thas already works at runtime and compiles without errors too, but is just not typesafe because
exremains typed asanythoughout so no typos will be found, and no refactorings will find symbol references in this code.The workaround is to simply create a local variable:
However it will not help with your desire of narrowing in any meaningful way (I think) 🌹
I was about to file something about this but I see there’s already an issue…
I would like to frame the problem this way. This is a really standard Javascript idiom (I’ve seen it recommended in pure-JS stackoverflow answers more than once; here’s one example):
Not only is it a common idiom, because of the limitations of Javascript as a language there is no better way to do it if you are doing exceptions with ES6 classes. In all other cases I’ve seen, Typescript takes common Javascript idioms and finds some way to make them typesafe, even if the result is a little awkward. However Typescript fails in this case to give me a convenient way to make a common, mandatory even, part of Javascript programming typeable. I think this means Typescript is not living up to its promise in this case, and it should be fixed somehow. That could mean type-narrowing on if ( x isinstance e ), it could mean a special ifinstance(x, e) or an explicitly narrowing variant of isinstance, it could mean bali182’s proposal.
I do like bali182’s proposal best because it would produce more convenient code that looks like other languages. It would shorten the simple catch clause in my example from six lines (eight, if I choose to introduce a second typed variable for e) to three. I don’t agree with the idea this interferes with Typescript’s goal of being an erasing compiler. There are other situations where Typescript introduces a syntax construct completely absent from javascript-- enum {} would be an example-- if it is necessary to fill out the type system. Multiple catch{}es are not legal Javascript and typed catches are not legal Typescript so this is not altering behavior based on type so much as introducing a new syntax construct that compiles to idiomatic Javascript. If there’s something philosophically unpleasant about the fact that what triggers the multi-catch is specifically a type annotation, maybe the syntax could be
catch (e instanceof CustomException)instead ofcatch (e : CustomException)or something else to make it very clear this is a TypeScript specific construct.Incidentally, there is a conditional catch clause syntax Firefox has introduced as a js extension and which may be relevant to be aware of here. However it is not on a standards track, and it is also very ugly.
It would be very nice if we could type-annotate the catch clause variable with
ex: Errorsince it’s perfectly sound to do so in many if not most cases. Consider:Erroror one of its a subclasses such asReferenceError.throwstatement somewhere in our own code or in third party code, that is executed within the guardedtryblock.Errorinstances.tryblocks that contain no third party code.tryblocks that reference third party libraries that very explicitly document that they only throwErrorinstances.The counter-argument that TypeScript should disallow the annotation in all circumstances because it could be incorrect in some circumstances is an interesting one:
…given that the obvious workaround is to exploit the “user knows best” philosophy that TypeScript applies to an equivalent annotation in a different position:
@mcclure I am saying that
instanceofis not reliable.instanceofis not a type test, it is a value test. It performs a reference comparison between the prototype properties of its operands. Because prototypes are mutable, any object can have its heritage rewired.@yortus you are correct that there plenty of other patterns that can be unreliable, but TypeScript is fairly agnostic with respect to whether you use them. As you point out this is a big thing to ask for, in other words, it is a new language feature. Adding a whole new language feature around
instanceofwould make the pattern seem clandestine. Regardless, I agree with your analysis that allowing type narrowing of catch variables makes sense.@mcclure you’re absolutely correct that this suffers from the same problems. The implication is that
instanceofis not reliable. Personally, I think it is an anti-pattern.The problem I have with @bali182’s suggested solution is that it bakes an unreliable pattern into the language, encouraging its use by providing manifest syntax, and providing a false sense of security to the programmer.
With #9999 a restriction is being lifted, but you still have to choose to shoot yourself in the foot and nothing encourages you to do so.
@aluanhaddad So, in your example, it seems like the Typescript type checker only does something incorrect because the ambient typings are incorrect. It seems like by nature Typescript will typecheck incorrectly if its ambient type declarations are incorrect.
This said, Typescript does not really do anything incorrect in your example.
eis not an instance ofAccessError, so it is correct the theAccessErrorclause does not trigger. The code itself has a bug-- but you would get the same bug, and it would be just as hard to find, if you were using plain JS instead of Typescript and the idiomatic Javascriptcatch (e) { if (e instanceof AccessError) { console.log("...") } }were used. (You could wind up writing this code if the documentation for the module exporting AccessError were incorrect, for example.) I do not see the generated behavior here as based on “a Type”; the behavior is based onCustomErrorin its capacity as a symbol in scope.Moreover, we would have all of these same problems both with the buggy behavior and the type checking if we wrote the idiomatic Javascript in a scenario where the narrowing instanceof (bug 9999 you reference) were implemented. If bug 9999 were implemented and one used the
e instanceof AccessError…else throwidiom with your code,ewould incorrectly type asAccessErrorwithin the narrowed scope. So although narrowing-instanceof sounds like a great proposal to me, I do not think your example is a argument that narrowing-instanceof is specifically superior to bali182’s proposal. In fact, if the ambient types in your example were accurate (markingAccessErroras a() => Error), then I think bali182’s proposal would perform slightly better on your example scenario than narrowing-instanceof. Narrowing-instanceof would allow thee instanceof AccessError(because things of type() => Errorare valid right-operands toinstanceof) and then just silently fail to type e inside the if scope. Your sample code would throw an error, but only because it happened to use one of the fields of what the code author believedAccessErrorto be. Bali182’s proposal on the other hand is a more constrained construct, and so it could check more aggressively and identify that} catch (e: AccessError) {is itself invalid code becauseAccessErroris not something which can ever be a type.once the exception caught what is that that you are going to do with it? a rhetorical question, but at the end of the day your options are limited:
@basarat BTW the hack I’m referring to is the need for both
errandexto get the job done. Someone reading this code (or, say, debugging the output js) would not understand why the second variable is required, without knowing something about the contentious history of narrowing withany.EDIT: -And- the workaround with both
errandexis subject to the same fallacy you mention about assuming an instance ofError.Yep, that part i agree with. What bothers me is the fact that narrowing doesn’t work with
any, and there’s no alternative to usinganyhere.