Home > Mobile >  Type guard two type of functions with same arguments but different return type
Type guard two type of functions with same arguments but different return type

Time:12-31

I would like to check if the passed type is the right type. See code example below. But I don't know how to check based on the return type because the arguments of FunctionA and FunctionB are the same.

type TypeA = { a: string };
type TypeB = { a?: string, b: Record<string, any> };

type FunctionA = ( argA: string ) => TypeA | Promise<TypeA>;

type FunctionB = ( argA: string ) => TypeB | Promise<TypeB>;

function isFunctionB(target: FunctionA | FunctionB): target is FunctionB {
  return true //How do I check the type without running the function
}
 
class Test {

    myFunction?: FunctionA | FunctionB;
    mustBeFunctionB: boolean;

    constructor({
        myFunction,
        mustBeFunctionB
    }: {
        myFunction?:  FunctionA | FunctionB;
        mustBeFunctionB: boolean;
    }) {
        this.myFunction = myFunction;
        this.mustBeFunctionB = mustBeFunctionB

        if (mustBeFunctionB && myFunction && !isFunctionB(myFunction)) {
            throw new Error(
                "Invalid constructor arguments! myFunction must be a type of FunctionB"
            );
        } else {
            console.log("Passed")
        }
    }
}

async function myFunctionA(argA: string): Promise<{ a: string }> {
  return { a: "test" };
}

const test = new Test({
  myFunction: myFunctionA,
  mustBeFunctionB: true
}); // This should fail but it still passed

CodePudding user response:

There's no way to check whether a function conforms to a given signature (including the return type) at runtime without executing it as the type information does not exist then.

The only way I can think of to solve this problem would be to use branded types, like so:

type TypeA = { a: string };
type TypeB = { a?: string, b: Record<string, any> };

type FunctionA = (( argA: string ) => TypeA | Promise<TypeA>) & { __brand: 'A' };

type FunctionB = (( argA: string ) => TypeB | Promise<TypeB>) & { __brand: 'B' };

// ...

const myFunctionA: FunctionA = async (argA: string): Promise<{ a: string }> => {
  return { a: "test" };
}

myFunctionA.__brand = 'A'

// ...

function isFunctionB(target: FunctionA | FunctionB): target is FunctionB {
  return target.__brand === 'B';
}

This will allow you to difference both types at runtime, given they are defined properly.

You might want to look at ts-brand if you're planning on reusing this sort of pattern more often.

CodePudding user response:

Solution 1: Method Overloading

You can make use of method overloading to achieve it. Here is a simplified code sample to present the concept.

// if mustBeFunctionB is true, myFunction should be FunctionB
function fun(myFunction: FunctionB, mustBeFunctionB: true): any
// if mustBeFunctionB is false, myFunction could be FunctionA or FunctionB
function fun(myFunction: FunctionA | FunctionB, mustBeFunctionB: false): any
// the base type defination, should be compatible with all types above
function fun(myFunction: FunctionA | FunctionB, mustBeFunctionB: boolean): any {

};

It can also be used for class constructor. See the modification of your code in playground


Solution 2: Union type parameter

You can also declare the input with union to specify the peer of the mustBeFunctionB flag if you okay with a single object config as in your code.

constructor({
    myFunction,
    mustBeFunctionB
}: {
    myFunction: FunctionB;
    mustBeFunctionB: true;
} | {
    myFunction?:  FunctionA | FunctionB;
    mustBeFunctionB: false;
})

playground


Beware that these solutions does not check the exact context on runtime, but just a "type guard" as you stated in the title. However, typescript will prevent you from making wrong input, which is the concept of typescript.

  • Related