Skip to content

type guard narrowing to a type with type with any in type parameters replaces any with unknown #57892

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

Closed
eps1lon opened this issue Mar 21, 2024 · 8 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@eps1lon
Copy link
Contributor

eps1lon commented Mar 21, 2024

πŸ”Ž Search Terms

type guard parameter any unknown

πŸ•— Version & Regression Information

  • This changed between versions 4.9.6 and 5.0.4

⏯ Playground Link

https://www.typescriptlang.org/play?ts=5.5.0-dev.20240321#code/JYOwLgpgTgZghgYwgAgEIHsAeAeAKgPmQG8AoZc5ANzgBsBXCALmVxIF8SSY6QExh0IZMADOGHAQAUAIyzMeAaxDoA7iACUzWZmEi0WPIVIVkUCGDpQhYKA3acEgkWGQg6AW2nRxzcdkXKaoQAvMTU9EwAjBwkwDDIkqJ+bp7Q+JIpXlDi6urEZBSZ3lgAdOF2JgD0lSYmAHoA-PZxCUkGcCAAnulF2Vi5+Sa94mW0FRTVtRSN7EA

πŸ’» Code

interface Box<T> {
    value: T
}

function isBox<T>(box: unknown): box is Box<T> {
    return true
}

const numberBox: Box<unknown> = {value:1}

if (isBox<number>(numberBox)) {
    numberBox.value
    //        ^?
}
if (isBox<any>(numberBox)) {
    numberBox.value
    //        ^?
}

πŸ™ Actual behavior

if (isBox<any>(numberBox)) {
    numberBox.value
    //        ^? (property) Box<unknown>.value: unknown
}

πŸ™‚ Expected behavior

if (isBox<any>(numberBox)) {
    numberBox.value
    //        ^? (property) Box<any>.value: any
}

Additional information about the issue

Maybe caused by #52282 which also caused #53178

@Andarist
Copy link
Contributor

Likely this is working as intended. There is a subtype relationship between Box<unknown> is a strict subtype of Box<any> and thus it's preferred when narrowing here. If the predicate's type would be preferred here then you would end up with Box<any> here and that's not desirable:

interface Box<T> {
  value: T;
}

function isBox(box: unknown): box is Box<any> {
  return true;
}

declare class Dog {
  bark(): void;
}

declare const smth: Box<number> | Dog;

if (isBox(smth)) {
  smth.value;
  //   ^? Box<number>
}

@eps1lon
Copy link
Contributor Author

eps1lon commented Mar 21, 2024

If the predicate's type would be preferred here then you would end up with Box here and that's not desirable:

If the type guard is typed as box is Box<any> then the any is clearly desired.

It's fine if TypeScript tries to guess missing intent. But here we authored it as "narrow this to any" so TypeScript shouldn't override it.

@Andarist
Copy link
Contributor

Narrowing is more like a filtering operation and not like a cast/assignment. Otherwise, you likely wouldn't be able to express "check if it's a Box and if it's a Box keep the type intact". We can check how this doesn't infer the type argument:

interface Box<T> {
  value: T;
}

function isBox<T>(box: unknown): box is Box<T> {
  return true;
}

declare class Dog {
  bark(): void;
}

declare const smth: Box<number> | Dog;

if (isBox(smth)) {
  // ^? function isBox<unknown>(box: unknown): box is Box<unknown>
  smth.value;
  //   ^? Box<number>
}

@RyanCavanaugh
Copy link
Member

If the type guard is typed as box is Box<any> then the any is clearly desired.

It really isn't. People in general hate any appearing in their code unless it's via some extremely direct incantation.

This has been the behavior since at least 3.3 and I don't think this is a) surprising, since we haven't gotten other reports on it or b) a welcome change to all the people who wrote declarations of the form is F<any> when they maybe should have written is F<unknown> but will now see an infectious any they didn't want.

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Mar 21, 2024
@RyanCavanaugh
Copy link
Member

I missed the regression part since I think I was working off the other example. I'll bisect to ensure we're on the same page here.

@RyanCavanaugh
Copy link
Member

Yeah, the 4.9.5 behavior is just inconsistent for no obvious reason:

interface Box<T> {
    value: T
}

function isBox(box: unknown): box is Box<any> {
    return true
}

declare const box1: string | Box<unknown>;
if (isBox(box1)) {
    box1.value
    //     ^?
    //     any
}

declare const box2: string | Box<{} | null | undefined>;
if (isBox(box2)) {
    box2.value 
    //     ^?
    //     {} | null | undefined
}

declare const box3: string | Box<string>;
if (isBox(box3)) {
    box3.value
    //     ^?
    //     string
}

It doesn't make sense to narrow from unknown to any but not any of the other types.

@fatcerberus
Copy link

fatcerberus commented Mar 22, 2024

note that IIRC Array.isArray(x) is typed as x is Array<any> - and obviously you don't want your values typed as e.g. number[] | number to be "narrowed" to any[]

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Mar 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

5 participants