Skip to content

Filter with a type guard fails to narrow when the type guard's predicate is a tuple supertypeΒ #59054

Closed as not planned

Description

@RobertSandiford

πŸ”Ž Search Terms

filter type guard failing to narrow tuple supertype

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried

⏯ Playground Link

Playground Link

πŸ’» Code

import { expectType } from "ts-expect";

declare function Tuple0IsNumberOrString(tuple: [unknown]): tuple is [number | string]
declare const t: [number | null] // t[0] is string | number
if (Tuple0IsNumberOrString(t)) {
    t[0] // predictably the type guard narrows t[0] to number
}

// but filter fails to narrow
const list = [t]
const filtered = list.filter(Tuple0IsNumberOrString)
const f = filtered[0]![0] // number | null

// strangely if the typeguard is changed, narrowing now works
declare function Tuple0IsNumber(tuple: [unknown]): tuple is [number]
const filtered2 = list.filter(Tuple0IsNumber)
const f2 = filtered2[0]![0] // number

More examples on the playground

πŸ™ Actual behavior

[number | null] & [number | string] generally gives a result of [number], but when a type guard is used with filter (specifically), the result is not the expected type of [number][], but rather the input type of [number | null][].

The problem occurs when the type guard's type assertion is a supertype of the expected item result type [number], but goes away if the type assertion is the same type or a subtype, i.e. [number] or [5]. The problem does not occur without the tuple wrapper.

πŸ™‚ Expected behavior

The filter result should be [number][]

Additional information about the issue

No response

Activity

  1. changed the title [-]Filter with a type guard fails to narrow when the type guard's assertion is a tuple supertype[/-] [+]Filter with a type guard fails to narrow when the type guard's predicate is a tuple supertype[/+] on Jun 27, 2024
  2. Andarist commented on Jun 27, 2024

    @Andarist
    Contributor

    This depends on the filter's definition that:

    filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];

    With your type predicate you end up with:

    T // [number | null]
    S // [number | string]

    This S doesn't satisfy this T so this overload can't be picked.

  3. RobertSandiford commented on Jun 27, 2024

    @RobertSandiford
    Author

    The second overload here allows the typeguard the work.

    interface Array<T> {
        filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];
        filter<V, S extends V>(predicate: (value: V, index: number, array: T[]) => value is S, thisArg?: any): (T&S)[];
    }

    TS Playground Demo

    The problem though is that T and may not be assignable to V, leading to unsafety. V needs a constraint that it is a supertype of T. e.g.

    type AllowedV<V, T> = T extends V ? V : 'not allowed'
    
    declare global {
        interface Array<T> {
            filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];
            filter<V, S extends V>(predicate: (value: AllowedV<V, T>, index: number, array: T[]) => value is S, thisArg?: any): (T&S)[];
        }
    }

    TS Playground with supertype constraint

    Seems like we need supertype constraints to make progress on this #14520

  4. RyanCavanaugh commented on Jul 9, 2024

    @RyanCavanaugh
    Member

    You would need to write this as

    declare function Tuple0IsNumberOrString<T>(tuple: [T]): tuple is [T & (number | string)]
  5. typescript-bot commented on Jul 12, 2024

    @typescript-bot
    Contributor

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

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

    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

      Issue actions