Skip to content

Assertion signature on generics doesn't narrow #60130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking β€œSign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
OfekShilon opened this issue Oct 3, 2024 Β· 13 comments Β· May be fixed by #60865
Open

Assertion signature on generics doesn't narrow #60130

OfekShilon opened this issue Oct 3, 2024 Β· 13 comments Β· May be fixed by #60865
Labels
Bug A bug in TypeScript Help Wanted You can do this
Milestone

Comments

@OfekShilon
Copy link

πŸ”Ž 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

https://www.typescriptlang.org/play/?noUncheckedIndexedAccess=true&allowUnusedLabels=true&downlevelIteration=true&noEmitHelpers=true&noUnusedLocals=true&noUnusedParameters=true&preserveConstEnums=true&removeComments=true&importHelpers=true&target=99&useUnknownInCatchVariables=false&exactOptionalPropertyTypes=true&noImplicitOverride=true&noFallthroughCasesInSwitch=true&noPropertyAccessFromIndexSignature=true&inlineSourceMap=true&inlineSources=true&stripInternal=true&ts=5.6.2#code/FAehAICIHEHtYCbgIYGdUFMBOAXSAucAW2QGsNVwEKcsBXAYxzqwEsA7Ac3FUYYwwJQEAGZ12TVrHYp02HAAoGhcaXawA7uwCUhNJlyUG4AN7DwFi6xHgFAQgbbT5y6-A4AFlk3h2GDeAAKgCeAA4YAKJY3lgKAOQAgnK4UjIiyKwANoJx2gDcLpYAvuYl5pAAQshI+vIEVDT0TCwc3NgxwGISOKmyBjgAPADCAHxKhEO6ffJGzq7Wtg5OZm6Wnt4BfgEh4VEx8Un9velZOfnAriVlGAAeobC44F2S0u40CpkcFISotK0A2gBdKa-NhcIHOC6rcDZHBPcTKObQ1zsZBEDA-P5cArI1yfPyoTFgzhAnHQorgAA+vjomUy4AAvDS6QUoasRA9bAxpL8YV9wLAbPiKMs2biFgoALLITwAOiwyHYCFgRAUTgG4AADLKAKyi3HQ56MpEG5Go9GESAARw02EgABoxaa3MLCeB-sLAY7nZcyQaKRhMpgTT7argFM9zj7LNz2LyTOaMPa+QSKUznnlLGBwAMGXnwBEAEqFgDyhfAAAkixEnabXbLQnRUB4Pl8o9HpuHE+A7Pm4sgAEYMXJ+3ElZHj6ESyO11ZYDDMLBpBGy12j1zzxcyNdiopAA

πŸ’» 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

@RyanCavanaugh
Copy link
Member

Why is assert written this way in the first place?

@RyanCavanaugh RyanCavanaugh added the Needs More Info The issue still hasn't been fully clarified label Oct 3, 2024
@OfekShilon
Copy link
Author

Why wouldn't it be? Does this violate any TS syntax?

@RyanCavanaugh RyanCavanaugh added Not a Defect This behavior is one of several equally-correct options and removed Needs More Info The issue still hasn't been fully clarified labels Oct 4, 2024
@RyanCavanaugh
Copy link
Member

Why wouldn't it be?

Because it's a generic type parameter that doesn't do anything.

@OfekShilon
Copy link
Author

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
Copy link
Author

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
Copy link
Contributor

jcalz commented 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 func after the assertion) but not really? If you replace the declaration of func from let func: XXX | null = null; to let func = null as XXX | null the 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
Copy link
Author

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
Copy link
Contributor

jcalz commented 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
Copy link
Author

@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.


  • since tsc doesn't complain, I'm equally as likely as the original author (probably more) to silently make this 'mistake'.

@jeremy-rifkin
Copy link

To add on to what @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 assert and 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 why assert<C>(c: C): asserts c is fundamentally wrong?

If this is intended behavior or a design limitation of the generic version, +1 to:

Silently failing seems buggy

@RyanCavanaugh RyanCavanaugh added Bug A bug in TypeScript Help Wanted You can do this and removed Not a Defect This behavior is one of several equally-correct options labels Oct 4, 2024
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Oct 4, 2024
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Oct 4, 2024 β€’

Someone can send a PR if they figure out what's going on here.

why it's an issue in the first place

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 asserts signature.

@RyanCavanaugh
Copy link
Member

Probably the right behavior is to say that an : asserts T function can't be generic

@Andarist
Copy link
Contributor

The assertion signature here is a slight red herring, the same happens for any inferrable signature:

declare function inferStuff<T>(arg: T, checkStuff?: boolean): T;

export function test(lines: string[]): string[] {
  let func: {
    name: string;
  } | null = null;

  for (const _ of lines) {
    if (Math.random() < 0.5) {
      func = {
        name: "qwer",
      };
    } else {
      if (func) {
        const { name } = func; // errors here
        func = inferStuff(
          {
            name: "other",
          },
          name === "abc",
        );
      }
    }
  }

  return lines;
}

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.


Probably the right behavior is to say that an : asserts T function can't be generic

One could have some generic asserts like:

declare function assertSmth<T extends { kind: string }>(arg: T, c: boolean): asserts c;

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 reasonable assert with a generic c too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Help Wanted You can do this
Projects
None yet
5 participants