Neither JavaScript nor TypeScript have "true" function overloads in the sense that you can have two different functions with the same name that differ by number/types of argument. TypeScript's overloads allow you to have multiple call signatures, but there's still just one function implementation, and that implementation needs to be written in such a way that it can handle all of the possible call signatures. it is not possible to "export functions with the same name but different parameters".
That means we need to try to write a single isType() function that can somehow differentiate between a ValidatorRetriever input and a ValidatorFunction input at runtime. If you can figure out a way to do this, then you can write a user-defined type guard function to let the TypeScript compiler know that's what you're doing. So it would look like this:
function isRetriever(v: ValidatorRetriever | ValidatorFunction): v is ValidatorRetriever {
/* implement this somehow */
}
The isRetriever() function takes an input that is either a ValidatorRetriever or a ValidatorFunction and returns a boolean value. If it returns true then the compiler knows that the input is actually a ValidatorRetriever; otherwise if it returns false then the compiler knows that the input is actually a ValidatorFunction. This lets you write isType() as follows:
function isType(validator: ValidatorRetriever | ValidatorFunction, data: unknown): boolean {
return isRetriever(validator) ? validator()(data) : validator(data)
}
So the question now is... how do we implement isRetriever()?
Unfortunately there's really no proper way to do this. JavaScript has a weak type system. You can't ask JavaScript about the parameter or return types of a function without calling it. There is a Function.length property which tells you the number of arguments that a function expects. So potentially you could just check whether v.length is 0 (for a ValidatorRetriever) or 1 (for a ValidatorFunction). And indeed this will work for simple cases:
function isRetriever(v: ValidatorRetriever | ValidatorFunction): v is ValidatorRetriever {
return v.length === 0;
}
function str() {
return (x: unknown) => typeof x === "string"
}
console.log(isType(str, "abc")) // okay, true
function num(x: unknown) {
return typeof x === "number"
}
console.log(isType(num, "abc")) // okay, false
But you can't rely on the length property this way. Functions in JavaScript can have rest parameters and accept any number of arguments, but this does not contribute to length:
function oopsieDoodle(...args: any[]) {
return args.length === 0
}
console.log(isType(oopsieDoodle, "abc")) // 💥 RUNTIME ERROR! validator() is not a function
The oopsieDoodle() function is a valid ValidatorFunction, since it does accept a single unknown argument (you can call oopsieDoodle(x) where x is of type unknown. Function type compatibility can sometimes be surprising) and returns a boolean. But our isRetriever() implementation erroneously decides that it's a ValidatorRetriever because the length property is 0. And we get a runtime error.
Maybe this isn't a big deal to you, and you'll accept that some inputs to isType() will cause runtime errors. If it is a big deal then you quickly run into unpleasant ways of trying to deal with it. Like, maybe you can test it by calling it and then seeing if the return type is a boolean or a function?
function isRetriever(v: ValidatorRetriever | ValidatorFunction): v is ValidatorRetriever {
return typeof v("TESTING") === "function";
}
This should work as long as v doesn't actually explode if you pass it an argument when it's not expecting one. And as long as calling it doesn't have unintended consequences (e.g., if it's stateful or slow). But passing dummy arguments to a function for the sole purpose of seeing what it does is a strange practice.
Maybe either of the above is acceptable to you for your particular use cases. In general, for future readers, it is not a great idea to try to probe functions at runtime for their types.
A better general solution is to refactor so that you are comparing things which have been explicitly marked with distinguishable properties, like a discriminated union:
type ValidatorRetriever = { (): (x: unknown) => boolean; type: "ret" }
type ValidatorFunction = { (x: unknown): boolean; type: "fun" }
function isType(validator: ValidatorRetriever | ValidatorFunction, data: unknown): boolean {
return validator.type === "ret" ? validator()(data) : validator(data)
}
function str() {
return (x: unknown) => typeof x === "string"
}
str.type = "ret" as const;
console.log(isType(str, "abc")) // okay, true
function num(x: unknown) {
return typeof x === "number"
}
num.type = "fun" as const;
console.log(isType(num, "abc")) // okay, false
function oopsieDoodle(...args: any[]) {
return args.length === 0
}
oopsieDoodle.type = "fun" as const; // <-- if you make this "ret" you'll see errors
console.log(isType(oopsieDoodle, "abc")) // okay, false
Instead of relying on built-in function behavior, you're adding a type property to the functions in question and looking at that to discriminate between a ValidatorRetriever and a ValidatorFunction.
Again, this might be unnecessary for your particular use cases, and if length or test calling the function is acceptable then you can do that. But discriminated unions are well-supported in TypeScript in a way that runtime function inspection is not.
Playground link to code
(...) => boolean? Please provide a minimal reproducible example that demonstrates your issue and only your issue when pasted as-is into a standalone IDE.There; can't you just remove it and ask the same question like this? The generics only seem to make the question more complicated without actually having much to do with the underlying issue: telling apart function types at runtimelengthproperty of the function like this, but it's fragile; there is no guarantee that a function of "zero" length acts the way you want. Does that address the question or am I missing something?