To understand what's happening here, we to need understand the ways in which the hof function is allowed to use the cb that is passed to it.
What arguments can be passed?
Let's consider a simpler example, consisting of only a single argument.
function hof(cb: (a: number | string) => number | string) {}
So, hof is stating that it expects a callback function, cb, that takes in a single argument that can be a number or a string, and it returns a string or a number.
Now, think of the restrictions that TS would apply on hof in terms of what it can pass to cb. So, hof is allowed to call cb with either a number or a string and this is okay because hof has stated that the cb that it receives should be able to handle both a number and a string.
But, if you pass the following double function to hof, TS would complain:
function double(a: number): number {
return 2 * a
}
And this makes sense because hof can call this double function with a string which might break the double function because it's meant to accept only numbers.
Also notice the place where the error is thrown, it's thrown right where hof is invoked with double, indicating that something is wrong with how hof is being called and not with how it's implemented.
function hof(cb: (a: number | string) => number | string) {}
function double(a: number): number {
return 2 * a
}
// @ts-expect-error
hof(double)
So, the callbacks passed to hof should be able to handle a superset of arguments that hof is allowed to pass to the callback.

How can the return type be used?
Now let's see the ways in which hof can use the value returned by cb.
Because cb can return either a number or a string, it's hof's responsibility to use the return value wisely, meaning that hof should not assume that the returned value is always a number or a string, hof needs to put appropriate type guards before making any such assumptions.
function hof(cb: (a: number | string) => number | string) {
// @ts-expect-error
const fixed = cb(100).toFixed() // wrong assumption that the returned value is always a number
}
function hof(cb: (a: number | string) => number | string) {
const ret = cb(100)
if (typeof ret === "number") { // appropriate check to make sure that it's a number
const fixed = ret.toFixed();
}
}
And again notice the place where the error is thrown, it's thrown inside the function body indicating that there's a problem with the implementation of the function.
The callback passed to hof can also just return a number and TS would be absolutely fine with that.
function hof(cb: (a: number | string) => number | string) {}
function double(a: number | string): number {
return 2 * Number(a)
}
hof(double)
So, the callbacks passed to hof should only return a subset of values that hof expects the callback to return.

Overload Signatures
The behavior with overload signatures is different, the purpose of overloading is to restrict the behavior of a function.
So, arguments and return values of overload signatures should always be a subset of the arguments and return values of the implementation signature.