TypeScript
TypeScript copied to clipboard
Predicate return type is lost when the predicate is passed as a parameter.
🔎 Search Terms
predicate return type passed parameter
🕗 Version & Regression Information
Latest to date.
⏯ Playground Link
https://www.typescriptlang.org/play/?target=99&jsx=0#code/JYOwLgpgTgZghgYwgAgGIHt0EZkG8CwAUAJAyYBcyAzmFKAOYDcRxV6AthGABYMD6EADZUIlAK4gA1iHQB3EM0IBfIkVCRYiFBnQAmPCzLpKIMewBG0RcTgye0PjwbipM+YpWE14aPCTIAITgoAxJzYJdpOQUiT1VCABMIBEFglAR0EBpkYKhKAAodHAAfNEx9UqCoAEoAbQBdRSIklLTkGAkEMGBM5G44Kj4jAB4AFQA+fLBKUerKMGRgKmRR5AgAD0gQBOXcdopkCSj5ZCVkAH4V5BMIADcreKIMrIWoHABeHKgoADoYUAS+X6gyM1QAhIxkAB6KFlbDIUo6fSAHg3AJH7j0IHRAXR6IGQCXQQwBwwACmtNhBtst8nxKLYAJ7VZDvcbIcyYQQQWyTAAOUAgCWACDgkEoJKZBBI-LAYigeNyfwB+T5AqFIog1Q88We2Sg+k+BKJ2yBAyGmHBkJhcJKcIqgWCyEAMuREIA
💻 Code
declare const arr: (Foo1 | Foo2 | Bar)[];
declare function has_foo<T>(t: T): t is T extends { foo: unknown } ? T : never;
const r1 = arr.find(has_foo)!; // Foo1 | Foo2 🎉
function do_find<P extends (_: any) => boolean>(predicate: P) {
return arr.find(predicate); // same line as in r1
}
const r2 = do_find(has_foo)!; // Foo1 | Foo2 | Bar ❌
Definitions of Foo1, Foo2 and Bar in the playground to reduce clutter.
🙁 Actual behavior
Predicate type narrowing lost.
🙂 Expected behavior
Predicate type narrowing preserved.
Additional information about the issue
Is it only because the predicate is passed as a parameter or is it something else 🤔
function do_find<P extends (_: any) => boolean>(predicate: P) { return arr.find(predicate); // same line as in r1 }
It's not the same. P is not defined as a predicate here, so a different find overload is resolved, which does not use predicates and does not narrow types.
// For reference
interface Array<T> {
find<S extends T>(predicate: (value: T, index: number, obj: T[]) => value is S, thisArg?: any): S | undefined;
find(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): T | undefined;
}
You need something like this to pass generic predicates:
function do_find<R extends typeof arr[number]>(predicate: (t: typeof arr[number]) => t is R) {
return arr.find(predicate);
}
A predicate function type is not the same as a function returning a boolean. You need to mimic what the find itself is doing with its overloads to make it work: TS playground
I had done
function do_find<P extends (_: any) => _ is any>(predicate: P)
to make it clear that P IS a predicate, and the result is even worse.
@bgenia I also had tried something similar to your solution but it didn't work.
function do_find<T, R extends T>(predicate: (t: T) => t is R) {
return arr.find(predicate); // error
}
The do_find function is not supposed to know about the typeof arr[number], so that one is not practical.
Also without the single type parameter the result is the same.
function do_find<R>(predicate: (t: unknown) => t is R) {
return arr.find(predicate);
}
To notice for reference, this one specifies the parameter is a predicate, and in the original, the whole parameter is under a generic type, and predicates do inherit from returning boolean, so I don't think find overload was the issue.
Your arr isn't represented in the parameters list so there is no way for TS to infer that type from anywhere here. do_find<Whatever1, Whatever2>(somePredicateForWhatever) is a valid invocation and you want the return arr.find(predicate); to error here because of that
@Andarist You provided the same solution, El = (typeof arr)[number].
The original point was that the whole type of the parameter was generic under P and maybe it's assumed that it should be brought complete, and be able to infer.
But if you say that it's not possible then ok. 🤷
The do_find function is not supposed to know about the typeof arr[number], so that one is not practical.
The answers are based on the examples you provided. do_find knows exactly what array type it works with, so it makes most sense to put that concrete type in the signature.
The original point was that the whole type of the parameter was generic under P and maybe it's assumed that it should be brought complete, and be able to infer.
This seems like a massive XY problem. What exactly are you trying to achieve?
The answers are based on the examples you provided.
Sorry, I'm not looking for answers or workarounds. I'm just making a report of how the type that is completely wrapped by the generic type parameter loses information... or any other way you could express it. The examples are a simplification to get the point across, not to be taken literally.
What exactly are you trying to achieve?
Like I said, just reporting. I always feel like there's an air of defensiveness though.
Like I said, just reporting. I always feel like there's an air of defensiveness though.
What you've initially reported is working as intended (boolean functions are not predicate functions).
If you still think that there is an issue to report I think you should restate it accordingly. What exactly is wrong? What behavior do you expect?
boolean functions are not predicate functions
ehr, I don't think that's how extends clauses in the type parameter work, like I mentioned before, they only determine the "upper bound" for the type and a predicate indeed satisfies this bound.
type Test<T> = T extends { (_: any): boolean } ? true : false;
type X = Test<typeof has_foo>; // true
The type is resolved upon invokation and typeof has_foo is what should be assigned to P as required by the parameter predicate: P.
The report is that somehow part of typeof has_foo is lost.
These also dont work:
function do_find<P extends (_: any) => _ is any>(predicate: P)
function do_find<S>(predicate: (_: any) => _ is S)
function do_find<T, S extends T>(predicate: (_: T) => _ is S): S
What exactly is wrong?
If you think there's nothing wrong, or that this is intended behavior, then no problem 👐 I misunderstood what predicate: P where P is typeof has_foo represents.
Close the issue and we're cool ✌️
The way you're writing it, it could only work if generic calls were "stamped out" into concrete calls, but they're not. The body of do_find has to be analyzed only in terms of its type parameters. I also don't understand what you're trying to do with the has_foo definition.
The correct way to do it looks like this
declare const arr: (Foo1 | Foo2 | Bar)[];
declare function has_foo(t: unknown): t is (Foo1 | Foo2);
const r1 = arr.find(has_foo)!; // Foo1 | Foo2 🎉
function do_find<P extends (typeof arr)[number]>(predicate: (_: any) => _ is P) {
return arr.find(predicate);
}
const r2 = do_find(has_foo)!; // OK
Thanks for trying to explain this is something that's not supported.
I don't understand you guys insisting that "trying to do something".
The issue was simple "typeof has_foo had information that got lost when received as parameter". The purpose of the examples are irrelevant.
But good news, I think I found the problem.
The information that gets lost is the conditional after the t is in the declaration of the predicate, perhaps because it's too deep or something, it gets elided or simplified.
Specifically the part of the constraint { foo: unknown }, so moving the declaration <T>(t: T): t is T extends { foo: unknown } ? T : never into an auxiliary interface, we can save the constraint so it doesn't get lost, like so:
interface AuxInterface<U> { <T>(t: T): t is T extends U ? T : never }
declare const new_has_foo: AuxInterface<{ foo: unknown }>;
But now we have to declare has_foo as a const because as a function there's no space to save it. The signatures are still the same.
Therefore the problem is in the declaration of the predicate, not in the receiving side... well not completely true.
Having function do_find<U>(predicate: { <T>(t: T): t is T extends U ? T : never }) would also omit the information after t is, so aux interface is needed here too.
function do_find<U>(predicate: AuxInterface<U>) {
return arr.find(predicate);
}
const r3 = do_find(new_has_foo)!; // Foo1 | Foo2 🎉
Notice that typeof arr[number] was not required anywhere 👍... even though you guys were so vehement about it.
https://www.typescriptlang.org/play/?target=99&jsx=0#code/JYOwLgpgTgZghgYwgAgGIHt0EZkG8CwAUAJAyYBcyAzmFKAOYDcRxV6AthGABYMD6EADZUIlAK4gA1iHQB3EM0IBfIkVCRYiFBnQAmPCzLpKIMewBG0RcTgye0PjwbipM+YpWE14aPCTIAITgoAxJzYJdpOQUiT1VCABMIBEFglAR0EBpkYKhKAAodHAAfNEx9UqCoAEoAbQBdRSIklLTkGAkEMGBM5G44Kj4jAB4AFQA+fLBKUerKMGRgKmRR5AgAD0gQBOXcdopkCSj5ZCVkAH4V5BMIADcreO8NPxQAQTF1gEkfTSRhgFVxnhkGNJtMVnNkAsllcNlsdsh-hcrjd7iFPC1UlB0plsiAILI+P1BkZKO8vj8XsM9qTDq5oqdxk0vIQAPSs9qdbq9BLoIagBIAyYAB2xCWACDgkEoe1BUxmkOhy1WcIg22WSMuq1R0FO1VCHRAXR6IGQvP52yF+VFEHFkulyHJ32eWiF+oIJGxYDEUFNuQAdDABdaxRKpRBqh54hksgsoPoALxmvlB7b5YlDTDVACEjGQ7LK2GQpR0FUCwWQgBlyIgx7JQADMyCT5tTCXy+MJGaMObzBaKxcL+kAPBuASP2iEA
If still considered intended behavior or not a defect, feel free to close.
This issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes.