Why does Bar
below return false
and how could one make it so that it behaves similarly to Foo
- which basically returns true if the type is a function?
// type Foo operates as I expected
type Foo = ((str: string) => void) extends (...args: any[]) => any ? true : false // true
// why does this function not extend (...args: unknown[]) => unknown?
type Bar = ((str: string) => void) extends (...args: unknown[]) => unknown ? true : false // false?
A hacky solution would be to simply use Foo
instead of Bar
are there any better options that don't use any
which is discouraged?
unknown
is the top type and so T extends unknown ? true : false
always returns true
, except for never
. any
behaves similarly.
CodePudding user response:
The arguments of the function behave differently from its return value in terms of subtyping.
Let's see it at more simple example:
type UniversalHandler = (arg: number | string) => number;
type NumberHandler = (arg: number) => number;
Here, number | string
is a kind of "top type" for the part of the TypeScript type system, so we can treat it (for the sake of specific example) the same as unknown
. Now, here're the functions of these two types:
const uni: UniversalHandler = (arg) => typeof arg === 'number' ? arg : arg.length;
const num: NumberHandler = (arg) => arg;
Now it should be obvious that uni
is also a NumberHandler
(it will just always take the "is number" path), but num
is not a UniversalHandler
- given a string, it will return a string, not a number. That is, the function which accepts the "top type" is assignable to the function which accepts its subtype - not the other way around.
This is called contravariance, and that's how the "subtyping by function argument" usually works.
Now, back to the example. You now see that the "top type among functions" must not accept the top type as an argument, since other functions will not be assignable to it. Instead, it must be the most restrictive in its arguments - it must accept the bottom type, that is, never
:
type Baz = ((str: string) => void) extends (...args: never[]) => unknown ? true : false // true
As for why it works with any
- the reason is that any
is not a "true" top type, it's a kind of hybrid. Everything is assignable to any
, which makes it similar to a top type, - but any
is assignable to anything, too, which makes it work wherever the bottom type is expected.