Skip to content

Inconsistent inferred type predicate for filter #61182

Description

🔎 Search Terms

filter type predicate

🕗 Version & Regression Information

This only makes sense since TS5.5, when inferred type predicates were introduced.

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/MYewdgzgLgBAhjAvDA2gcigU2mgNDMAVwBtiBdAKAHoqY6YA9AfgotElgCMl4A6AMwCWxLACcAFAA8kAPhjSAhImRFSASmq16zVu2gxgPOAOFips+TCUqSxGADJ783tDiioEAOqCoAC3EYaGoaNPSMLBRAA

💻 Code

const a = ['test', null]

const b = a.filter(x => x !== null)

const c = a.filter(x => x !== null && x.startsWith('t'))

🙁 Actual behavior

On the above, the type of b is correctly inferred as string[], but the type of c is incorrectly inferred to (string | null)[]

🙂 Expected behavior

The inferred type of c should string[], just like the inferred type of b.

Additional information about the issue

No response

Activity

  1. MartinJohns commented on Feb 15, 2025

    @MartinJohns
    Contributor

    This is working as intended. Only for specific cases the type guard will be inferred. See #57465, the "Interesting cases" section.

    The type guard must be true for all values of the narrowed type, but your second function does not fulfill that purpose. The related issue to tackle this limitation is #15048.

    Here's an example of the problem: Playground link

    const fn = (x: string | null): x is string => x !== null && x.startsWith('t')
    
    declare const value: string | null
    if (fn(value)) {
        value;
        // ^?
    }
    
    if (!fn(value)) {
        value;
        // ^?
    }

    In the second if-check the type is narrowed to null, because the type guard checks if the value is a string - and if it's not it must be null. Note that there's nothing special about the function being passed to filter.

  2. pedrolamas commented on Feb 15, 2025

    @pedrolamas
    Author

    Thanks for your comment Martin Johns (@MartinJohns), I guess the main problem is the "else" of the type inference, as it can still be a string.

    I think that .filter() is indeed a special case, as the "else" inference is irrelevant (it excludes any value that would make the predicate false), but I guess that currently that is not possible given how inferred predicates work.

    Manual type inference it is then!

    const c = a.filter((x): x is string => x !== null && x.startsWith('t'))
  3. MartinJohns commented on Feb 15, 2025

    @MartinJohns
    Contributor

    I think that .filter() is indeed a special case

    The team (which I'm not part of, just pointing out) does not like to hardcode special cases. This would lead to confusion, as people would expect the same behavior for their own types and implementations. Think of a filter() function for an Observable implementation. It would be very confusing if the inference for type guards would work different here from the Array.filter() case.

    So without #15048 this remains unsolved.

  4. typescript-bot commented on Feb 27, 2025

    @typescript-bot
    Contributor

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

  5. locked as resolved and limited conversation to collaborators on Oct 22, 2025
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

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions