Technically speaking the compiler is right to complain. You can always use a type assertion to suppress the error on the default function parameter, as well as providing a default type parameter for T so the compiler knows what to do if it can't infer T from the argument:
function someFunction<T = number>(
f: (s: string) => T = ((s: string) => Number(s)) as any
) {
return function (s: string): T {
return f(s)
}
}
And this will behave exactly as you want:
const fNum = someFunction()
console.log(fNum("3.14159").toFixed(2)); // 3.14
const fStr = someFunction((s: string) => s)
But the problem with this approach is that the type parameter T is specified by the caller of someFunction(), not by the implementation. And so nothing will prevent a malicious and/or confused caller from doing something like this:
const fOops = someFunction<string>();
The caller has specified that T is a string, and even though T's default is number, it has been overridden by an explicit string type. So the compiler thinks that fOops is of type (s: string) => string, but of course at runtime we know it won't be:
try {
fOops("oopsie").toUpperCase(); // no compiler error
} catch (e) {
console.log(e); // fOops(...).toUpperCase is not a function
}
And that's why the compiler complains; it is saying that it can't be sure that the default value of f will be of a type appropriate for the T specified by the caller.
If you don't care about this possibility, then this approach is fine and requires the minimum change to your code.
If you want to prevent the possibility of an fOops-like situation, you could change someFunction() to be an overloaded function to have two call signatures:
// call signatures
function someFunction(): (s: string) => number;
function someFunction<T>(f: (s: string) => T): (s: string) => T;
If you call someFunction() with no argument, then you are invoking the first call signature, and the return type must be (s: string) => number; there's no generic type parameter on this call signature at all.
Otherwise, if you call it with a callback argument, then you are invoking the second call signature; there's a type parameter T corresponding to the return type of that callback.
Then the implementation can be similar to before, but you loosen the types up to prevent a complaint (here we will just use any as the return type):
function someFunction(f: (s: string) => any = (s: string) => Number(s)) {
return function (s: string) {
return f(s)
}
}
Now the calls you intend to support still work:
const fNum = someFunction()
console.log(fNum("3.14159").toFixed(2)); // 3.14
const fStr = someFunction((s: string) => s)
but it is impossible to invoke the zero-arg call signature with a type parameter, so you can't call it the wrong way anymore:
const fOops = someFunction<string>() // error!
// Expected 1 arguments, but got 0.
Playground link to code