Home > Net >  Understanding `extends (...args: unknown[]) => unknown`
Understanding `extends (...args: unknown[]) => unknown`

Time:07-13

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.

  • Related