Skip to content

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

New issue

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

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Closed
RobertSandiford opened this issue Jun 27, 2024 · 4 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@RobertSandiford
Copy link

RobertSandiford commented Jun 27, 2024

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

@RobertSandiford RobertSandiford 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 Jun 27, 2024
@Andarist
Copy link
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.

@RobertSandiford
Copy link
Author

RobertSandiford commented Jun 27, 2024

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

@RyanCavanaugh
Copy link
Member

You would need to write this as

declare function Tuple0IsNumberOrString<T>(tuple: [T]): tuple is [T & (number | string)]

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Jul 9, 2024
@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 Jul 12, 2024
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

4 participants