TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

'A type predicate's type must be assignable to its parameter's type' should not fire when in a generic predicate function

Open Nokel81 opened this issue 1 year ago • 5 comments

Bug Report

🔎 Search Terms

  • 2677
  • predicate
  • user defined
  • generic function

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about predicates

⏯ Playground Link

Playground link with relevant code

💻 Code

type ExtractOr<T, U, Fallback = never> = T extends U ? T : Fallback;

export function isFunction<T>(val: T): val is ExtractOr<T, Function, (...args: unknown[]) => unknown> {
  return typeof val === "function";
}

🙁 Actual behavior

Should be a valid user defined predicate which can narrow union types to just those variants that are functions, otherwise (specifically if val is unknown)

For example:


function foo(x: unknown) {
  if (isFunction(x)) {
    // x should be narrowed to `(...args: unknown[]) => unknown`;
  }
}

function foo(x: number | undefined | ((val: string) => boolean)) {
  if (isFunction(x)) {
    // x should be narrowed to `(val: string) => boolean`;
  }
}

🙂 Expected behavior

The TSC reports error 2677.

Nokel81 avatar May 03 '23 18:05 Nokel81

Pretty sure this ends up as a duplicate of #33912.

MartinJohns avatar May 03 '23 18:05 MartinJohns

I don't think it's a dupe of #33912. That one's about relating the return statement(s) in the function to the return type via control flow; this is just a function signature being rejected outright, before CFA even comes into play.

fatcerberus avatar May 03 '23 18:05 fatcerberus

@fatcerberus Maybe. My thought was that the type assignability error appears because the conditional type is not resolved.

MartinJohns avatar May 04 '23 07:05 MartinJohns

The error message accurately describes what's going on and I think the motivation for that error is pretty straightforward. the only thing you need to do to this code is add this intersection:

      val is T & ExtractOr

which shouldn't meaningfully change the behavior of the function at all

RyanCavanaugh avatar May 04 '23 20:05 RyanCavanaugh

@RyanCavanaugh Unfortunetly that does not work as intended and does meaningfully change the behvaviour. Here is a playground link

type ExtractOr<T, U, Fallback = never> = T extends U ? T : Fallback;

export function isFunction<T>(val: T): val is T & ExtractOr<T, Function, (...args: unknown[]) => unknown> {
  return typeof val === "function";
}

function foo1(x: unknown) {
  if (isFunction(x)) {
    type Y = typeof x;
    // x should be narrowed to `(...args: unknown[]) => unknown`;
  }
}

function foo2(x: number | undefined | ((val: string) => boolean)) {
  if (isFunction(x)) {
    type Y = typeof x;
    const b: boolean = x(""); // <--- error here, x("") returns unknown
    // x should be narrowed to `(val: string) => boolean`;
  }
}

Nokel81 avatar May 04 '23 20:05 Nokel81

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

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

@RyanCavanaugh why is this closed? I have shown why your "solution" doesn't work.

Nokel81 avatar May 07 '23 23:05 Nokel81

Hello! 👋

Maybe it's too late, but here is the solution (playground link)

type Func<TParams extends readonly any[] = any[], TReturn = unknown> = (...args: TParams) => TReturn;

type ExtractOr<T, U, Fallback> = Extract<T, U> extends never ? (Fallback extends T ? Fallback : never) : Extract<T, U>

export function isFunction<T>(val: T): val is ExtractOr<T, Func, Func<unknown[]>> {
  return typeof val === "function";
}

The original problem comes from the posible situation, when some given T doesn't intersect with Function type completely (for example type number | string has no overlap with Function) which leads to the resulting type like (val: number | string) => val is Function, but it's incorrect due to TS compiler rule 2677: "A type predicate's type must be assignable to its parameter's type".

So you need to handle this situation: ensure that Fallback type actually extends T before returning it from your generic ExtractOr or return never as a Predicate type otherwise (just like in the example above).

solovevserg avatar Dec 07 '23 16:12 solovevserg

ah thanks, will take a look

Nokel81 avatar Dec 07 '23 16:12 Nokel81