Function body is not checked against assertion signature
See original GitHub issueTypeScript Version: 3.7.0-dev.20191002
Search Terms: assertion signatures, asserts, not checked, unchecked, unsound
Code
function assertIsString(x: number | string): asserts x is string {
}
function test(x: number | string) {
assertIsString(x);
x.toUpperCase();
}
test(4);
Expected behavior:
This incorrect empty implementation of assertIsString
should not type check, because there are execution paths through assertIsString
that don’t narrow the type of x
to string
as the type of assertIsString
should require.
Actual behavior:
No TS errors. Compiled JS code fails at runtime: TypeError: x.toUpperCase is not a function
.
Playground Link: Playground nightly isn’t new enough, but maybe it will be by the time you read this: link.
Related Issues:
Issue Analytics
- State:
- Created 4 years ago
- Reactions:9
- Comments:5 (3 by maintainers)
Top Results From Across the Web
Overloads not typechecking body - typescript - Stack Overflow
No it does not check the body against overload declarations. As you've seen, it checks that implementation signature is compatible with all ...
Read more >Assertion Functions or Assertion Guards - TypeScript ...
The reason why assertion functions are also known as assertion guards is because of their similarity to type guards. In our type guard...
Read more >single sign on issue The Signature in the assertion is not valid
We have an issue where we are attempting to use SSO but it is erroring in Salesforce. The Certifcates have not expired.
Read more >Assertion functions in TypeScript - LogRocket Blog
Assertion functions in TypeScript are a very expressive type of function whose signature states that a given condition is verified if the ......
Read more >narrowing types via type guards and assertion functions - 2ality
Checking the result of typeof and similar runtime operations are called type guards. Note that narrowing does not change the original type of ......
Read more >Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start FreeTop Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Top GitHub Comments
FTR, the existing type guards feature suffers from the same soundness hole (which, as far as I can tell, isn’t called out at all in the documentation!):
but I’m filing this against the new feature in the hope that there’s still time to give this more thought before it becomes stable. I can file another report for type guards if desired.
Perhaps these unchecked features were justified with the rationale of allowing one to explain invariants that the checker wouldn’t derive by itself. But I would argue that they are also useful for encapsulating checks that the checker would derive if they weren’t encapsulated, and that these purposes should be separate: it should be possible to encapsulate checks without bypassing the type system.
If one does need to bypass the type system, one can already do it explicitly with casts. Bypassing the type system should be explicit.
Here’s a fully-worked example of how that happens. Inevitably it’s rather longer than the boiled-down example @andersk gave at the top. I hope it’s nevertheless helpful for priming one’s intuition for how a bug of exactly this kind happens in a real codebase.
The example starts from exactly the types the documentation uses as an example for type guards. I’ve just adapted the code slightly to use assertion functions instead.
First, the types:
These are verbatim from the docs’ example, except I’ve added an alias for the union type that the example mentions several times.
Now an assertion function, which is a direct translation of the type guard in the docs’ example:
Then here’s an example of using the assertion function:
At this stage, the assertion function is 100% correct, and all this code is quite reasonable.
Now, someone comes along and adds another type of pet, extending the union:
This is a perfectly reasonable change. Naturally they go on to update all the code that needs to change to handle the new
Amoeba
case, and they lean on the type-checker to help them be sure they’ve covered all of it.They don’t make any change to
assertIsFish
because they don’t notice it – it might be in a different file, or even a completely different project. And the type-checker doesn’t give a peep about it.But, in fact,
assertIsFish
is now wrong! It’ll accept a pet that’s anAmoeba
, as well as one that’s aFish
.As a result,
f
can blow right past theassertIsFish
call even though itsfish
isn’t a fish at all, but anAmoeba
. It’ll run through the subsequent logic with that assumption violated, and e.g. blow up with a runtime type error atfish.layEggs()
.Here’s fully stitched-together versions of this example on the playground: before; after.
Unlike the
Amoeba
author, the type-checker looks at the definition ofassertIsFish
. And it has all the information it needs to see that the assertion function is now wrong – it’s not fundamentally different from the type checking that it does everywhere else. So it should be able to spot this bug in a straightforward assertion function like this one.OTOH if I do write an assertion function in a way the type-checker doesn’t understand, then as @andersk says, the bypass should be something explicit like a cast.
PS: One might notice in this example that things only actually go wrong once there’s also a bug somewhere to break the invariant that the fish in
f
will actually be aFish
. But that’s just the nature of assertions – they only do anything when there’s already a bug somewhere – so it doesn’t really mitigate this issue. And in fact an analogous example with a type guard instead (if (isFish(pet)) fish.layEggs();
) will already go wrong onceAmoeba
is added, with no exogenous bug involved at all.