2
votes

How can I properly infer the return type based on a function that has one parameter that is a union of two types?

I've tried the following with conditional types, but it does not work (see inline comment for the typescript error):

TypeScript Playground

type Status = 'statusType'
type GoalActivity = 'goalActivityType'

type Argument = { type: 'status'; status: Status | null } | { type: 'goalActivity'; goalActivity: GoalActivity | null }

const handleReaction = (arg: Argument): Argument extends { type: "status" } ? Status : GoalActivity => {
    if (arg.type === 'status') {
        return 'statusType' // Type '"statusType"' is not assignable to type '"goalActivityType"'.
    } else {
        return 'goalActivityType'
    }
}

I've also tried the following using a form of function overloading for arrow functions (as described here), but this also results in a TypeScript error and also uses "any" which loses most of the typing benefits inside the function definition:

TypeScript Playground

type Status = 'statusType'
type GoalActivity = 'goalActivityType'

type HandleReaction = {
    (arg: { type: 'status'; status: Status | null }): Status
    (arg: { type: 'goalActivity'; goalActivity: GoalActivity | null }): GoalActivity
}

const handleReaction: HandleReaction = (arg: any) => { // Type '"goalActivityType"' is not assignable to type '"statusType"'.
    if (arg.type === 'status') {
        return 'statusType'
    } else {
        return 'goalActivityType'
    }
}

This question is similar to this one, but with the difference being that the function parameter is an object.

1

1 Answers

1
votes

Issue

The first thing is you haven't used generic type for your argument that would result in typescript will never infer the correct type based on your input (you can imagine generic type is parameter, tsc requires it to calculate the result based on your input).

In short,

const handleReaction = (arg: Argument): Argument extends { type: "status" } ? Status : GoalActivity => { // ... }

will always return Status | GoalActivity as return type.

Solution

Of course, you have to use generic type here as your argument. I'll split your code out with inline explanation:

type Status = 'statusType'
type GoalActivity = 'goalActivityType'

type StatusObj = { type: 'status'; status: Status | null };
type GoalActivityObj = { type: 'goalActivity'; goalActivity: GoalActivity | null }

type Argument = StatusObj | GoalActivityObj;

// Define returned type based on a input argument `T`
type ReturnType<T> = T extends StatusObj ? Status : GoalActivity;

// Generic type should be used here
const handleReaction = <T extends Argument>(arg: T): ReturnType<T> => {
    if (arg.type === 'status') {

        // Q: Why do we have to cast here?
        // A: Any returned type can't assign to statement of `type ReturnType<T> ...`
        // but luckily `tsc` still allows us to cast back since they are all string literal
        return 'statusType' as ReturnType<T>;

    } else {
        return 'goalActivityType' as ReturnType<T>;
    }
}