Skip to content

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

Not planned
@eps1lon

Description

@eps1lon

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

Activity

Andarist

Andarist commented on Mar 21, 2024

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

eps1lon commented on Mar 21, 2024

@eps1lon
ContributorAuthor

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

Andarist commented on Mar 21, 2024

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

RyanCavanaugh commented on Mar 21, 2024

@RyanCavanaugh
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 commented on Mar 21, 2024

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

RyanCavanaugh commented on Mar 21, 2024

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

fatcerberus commented on Mar 22, 2024

@fatcerberus

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

typescript-bot commented on Mar 25, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Not a DefectThis behavior is one of several equally-correct options

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @fatcerberus@RyanCavanaugh@Andarist@eps1lon@typescript-bot

        Issue actions

          type guard narrowing to a type with type with `any` in type parameters replaces `any` with `unknown` Β· Issue #57892 Β· microsoft/TypeScript