Home > database >  How to differentiate between function types in TypeScript
How to differentiate between function types in TypeScript

Time:09-01

I need to create type guards that are nested, and as such, they require either a function that returns the result of type checking isOk(data) => boolean, or a function that returns a function that returns the result of type checking isOk()(data) => boolean. This has to be done to allow the type checking to be nested and reference to either itself it two objects to each other.

This has been implemented all well and good, but no matter what I try I can't seem to find out how to differentiate between those two function types if it is given as an argument.

For example, exporting this is not possible as typescript prevents exporting values with the same name.

Minimum reproducible 1

export type ValidatorFunction = (data: unknown) => boolean;
export type ValidatorRetriever = () => ValidatorFunction;

export function isType(validator: ValidatorRetriever, data: unknown): boolean {
    return validator()(data);
}

export function isType(validator: ValidatorFunction, data: unknown): boolean {
    return validator(data);
}

similarly using instanceof is not possible, as it is used only for classes and not types, as types are discarded during transpile.

Minimum reproducible 2

export type ValidatorFunction = (data: unknown) => boolean;
export type ValidatorRetriever = () => ValidatorFunction;

export function isType(validator: ValidatorRetriever | ValidatorFunction, data: unknown): boolean {
    if(validator instanceof ValidatorRetriever) {
        return validator()(data);
    } else if(validator instanceof ValidatorFunction) {
        return validator(data);
    }
    return false;
}

Either the same function name or type detection must exist, as some objects may require either one or both of different validation functions, such as

let object2Validator = undefined;

const object1Validator = object1({
  key: isString, // which would be ValidatorFunction
  children: isArray(() => object2Validator) // which would be ValidatorRetriever
});
object2Validator = object2({
  path: isString,
  children: isArray(object1Validator)
});

if(object2Validator({...})) {
  console.log("Is object 2");
} else {
  console.log("Is not object 2");
}

The code examples are very barebones, but the main problem is the following: how to make either one of the previous two minimum reproducibles work?

A. export functions with the same name but different parameters (like isArray in the example)

Minimum reproducible 1 from above

or

B. differentiate between two different function types ValidatorFunction and ValidatorRetriever (alternative to different function implementations)

Minimum reproducible 2 from above

CodePudding user response:

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")) //            
  • Related