Repository navigation
Assertion signature on generics doesn't narrowΒ #60130
Copy link
Copy link
Open
Labels
BugA bug in TypeScriptA bug in TypeScriptDomain: check: Control FlowThe issue relates to control flow analysisThe issue relates to control flow analysisHelp WantedYou can do thisYou can do this
Milestone
Description
π Search Terms
Assertion AND signature
π Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about Assertion Signatures, Generics.
β― Playground Link
π» Code
// "Good assert": makes destructuring succeed
// function assert(c: unknown): asserts c {
// if (!c) {
// throw new TypeError('Assertion failed');
// }
// }
// "Bad assert": destructuring error
function assert<C>(c: C): asserts c {
if (!c) {
throw new TypeError('Assertion failed');
}
}
export function test(lines: string[]): string[] {
let func: {
name: string;
lines: string[];
} | null = null;
for (const line of lines) {
if (Math.random() < 0.5) {
func = {
name: "qwer",
lines: [line],
};
} else {
assert(func);
const {name, lines} = func; // <=== ERROR HERE: 'name' implicitly has type 'any'
lines.push(line);
assert(name !== 'abc');
}
}
if (func)
return func.lines;
return lines;
}π Actual behavior
When the generic-assert implementation is active, the line -
const {name, lines} = func;errors with 'name' implicitly has type 'any'.
When the unknown assert implementation is active, types are properly inferred.
π Expected behavior
Both assert versions contain assertion signature, and both should be able to narrow the type.
Additional information about the issue
No response
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
BugA bug in TypeScriptA bug in TypeScriptDomain: check: Control FlowThe issue relates to control flow analysisThe issue relates to control flow analysisHelp WantedYou can do thisYou can do this
Activity
RyanCavanaugh commentedon Oct 3, 2024
Why is
assertwritten this way in the first place?OfekShilon commentedon Oct 3, 2024
Why wouldn't it be? Does this violate any TS syntax?
RyanCavanaugh commentedon Oct 4, 2024
Because it's a generic type parameter that doesn't do anything.
OfekShilon commentedon Oct 4, 2024
As most bug repros, it is a narrowed down snippet. I personally don't expect these to make programming sense.
We do have a workaround (akin to the 'unknown' version above), but I believe neither the existence of a workaround nor the lack of program-sense in the example snippet make this any less of a bug.
OfekShilon commentedon Oct 4, 2024
BTW If actually fixing it is for some reason infeasible, it is an entirely legit fix to document this as an assertion-signature limitation. But until either of these are taken - people will continue to be stung by such unexpected behavior. Please reconsider before ignoring this as 'not a defect'.
jcalz commentedon Oct 4, 2024
Personally I'd expect any assertion signature to either narrow or produce an error as per #33622. Silently failing seems buggy. I'd be interested in seeing a more motivating use case, though.
The behavior here is weird, though, it seems that it is narrowing (if you hover over
funcafter the assertion) but not really? If you replace the declaration offuncfromlet func: XXX | null = null;tolet func = null as XXX | nullthe assertion does happen. So some fun stuff is going on with the CFA. It does look like there's a bug somewhere, but again, a motivating use case would really help.OfekShilon commentedon Oct 4, 2024
I'd say the wish to not lose half a day over trying to silence ts complaints where there shouldn't have been any AND there are zero indications to any problem in some use of assertion-signature somewhere in your codebase, should suffice as motivation.
jcalz commentedon Oct 4, 2024
Sure, great. But, I'm interested in where this actually shows up. I'm asking for something closer to the real use case. I get it, you don't think you should have to provide that. Maybe you're right. But I don't understand the pushback here. Anyway, I'll disengage. I hope to see this reclassified as a bug, if only for the silent failure.
OfekShilon commentedon Oct 4, 2024
Joe Calzaretta (@jcalz) sorry I didn't mean to come across as harsh. I really don't have a good use case, this is just how I found my codebase* and was expecting help from the compiler to handle it - either compiling it properly or complaining, but not silently perform an erroneous compilation. I don't think these are extravagant expectations and am quite confused as to the reaso s for marking this as a non-defect.
jeremy-rifkin commentedon Oct 4, 2024
To add on to what Ofek (@OfekShilon) shared (we're both contributors to the Compiler Explorer project), I don't think we have a compelling motivating use case where it must be written with a generic. I had just written it like this and it worked fine everywhere except this one puzzling place. I'd be happy to link to our code where we're defining
assertand where this strange behavior pops up if that'd be helpful.We can work around this by updating the assertion in our code to be
assert(c: unknown): asserts c, but it would be nice to better understand why this fixes the issue and why it's an issue in the first place. I'm not a typescript expert by any means but is there a reason whyassert<C>(c: C): asserts cis fundamentally wrong?If this is intended behavior or a design limitation of the generic version, +1 to:
RyanCavanaugh commentedon Oct 4, 2024
Someone can send a PR if they figure out what's going on here.
I don't think anyone's ever tried to write this code before, because there's nothing you can do with a type parameter that is also meaningful in the context of an
assertssignature.RyanCavanaugh commentedon Oct 4, 2024
Probably the right behavior is to say that an
: asserts Tfunction can't be genericAndarist commentedon Dec 27, 2024
The assertion signature here is a slight red herring, the same happens for any inferrable signature:
But perhaps, here, it's easier to see that it can, indeed, create some circularity. The circularity itself happens when checking the argument's type when inferring into a signature. Its type has to be obtained as it's a potential source for that inference.
In this particular case, it could be avoided because a binary expression with
===operator can only result in a boolean type. The compiler doesn't really have to check types of its operands to infer that.One could have some generic asserts like:
In this one, the
c's type itself is not generic so that could be validated but I'm pretty sure one could create a somewhat reasonableassertwith a genericctoo.