TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Predicate return type is lost when the predicate is passed as a parameter.

Open miguel-leon opened this issue 1 year ago • 12 comments

🔎 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 🤔

miguel-leon avatar Oct 18 '24 07:10 miguel-leon

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);
}

bgenia avatar Oct 18 '24 08:10 bgenia

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

Andarist avatar Oct 18 '24 08:10 Andarist

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.

miguel-leon avatar Oct 18 '24 08:10 miguel-leon

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

miguel-leon avatar Oct 18 '24 08:10 miguel-leon

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 avatar Oct 18 '24 08:10 Andarist

@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. 🤷

miguel-leon avatar Oct 18 '24 08:10 miguel-leon

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?

bgenia avatar Oct 18 '24 13:10 bgenia

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.

miguel-leon avatar Oct 18 '24 13:10 miguel-leon

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?

bgenia avatar Oct 18 '24 13:10 bgenia

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 ✌️

miguel-leon avatar Oct 18 '24 14:10 miguel-leon

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

RyanCavanaugh avatar Oct 18 '24 17:10 RyanCavanaugh

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.

miguel-leon avatar Oct 19 '24 04:10 miguel-leon

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 avatar Oct 22 '24 01:10 typescript-bot