TypeScript
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
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.
Pretty sure this ends up as a duplicate of #33912.
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 Maybe. My thought was that the type assignability error appears because the conditional type is not resolved.
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 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`;
}
}
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.
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).
ah thanks, will take a look