As soon as you introduce any
, you lose a lot of the type safety guarantees in TypeScript. The any
type is intentionally unsound to allow you to do things that the compiler thinks are unsafe. Everything is both assignable to and assignable from any
; it's an escape hatch from the type system. Therefore it's not surprising that NumberFunc
is seen as assignable to AnyFunc
. You'd also have seen that AnyFunc
is assignable to NumberFunc
:
type AnyNumber = CheckExtends<AnyFunc, NumberFunc> // AnyFunc
It's best to ignore any
then, and move on to the other part of your question using the type-safe unknown
instead: why isn't NumberFunc
assignable to UnknownFunc
?
In order to preserve type safety, functions should be contravariant in their parameter types. This means that the subtype/supertype relationship between function types is opposite that of their parameters. So, for example, while number
is a subtype of unknown
, a function of type (x: number) => void
is a supertype of (x: unknown) => void
. So the assignability relationship has flipped. This might be confusing at first but it actually is the only thing that makes sense from a type safety perspective. This is a consequence of what's known as the Liskov Substitution Principle; if A
is assignable to B
, it means that you can replace any value of type B
with one of type A
and it won't cause any errors or problems.
Let's say I have a value n
of type number
. If you ask for a value of type unknown
, you'll be happy if I give you n
. You'd have been happy if I handed you "hello"
, or false
, or any value. Because number
is a subtype of unknown
, you can use a number
anywhere you need an unknown
.
But now imagine that I have a value f
of type (x: number) => void
. If you ask for a value of type (x: unknown) => void
, and I hand you f
, you might be very unhappy when you try to use it. You think you have a function which accepts any possible parameter, so you expect to be able to call it like f("hello")
, or f(false)
, or f(15)
. But f()
only accepts number
parameters, so f("hello")
and f(false)
are very likely to explode at runtime. The function you're asking for is very specific. Because you can't use an (x: number) => void
in some places you can use an (x: unknown) => void
, then the former is not a subtype of the latter.
In fact it's the reverse: if I have a value g
of type (x: unknown) => void
, and you ask for a value of type (x: number) => void
, I can hand you g
and you will be happy. You will only be calling g()
with numeric parameters like g(1)
, g(2)
, or g(15)
, so you'll never have a problem at runtime. That means that UnknownFunc
is a subtype of NumberFunc
:
type UnknownNumber = CheckExtends<UnknownFunc, NumberFunc> // UnknownFunc
Okay, hope that helps; good luck!
Playground link to code