Robert Cooper
Robert Cooper

Reputation: 2240

Typescript return type depending on object parameter

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.

Upvotes: 2

Views: 4208

Answers (2)

Christian Jensen
Christian Jensen

Reputation: 965

Your argument objects should have their own types. Once they do, you can use function overloading like this:

type Status = { type: 'status', status: 'statusValue' };
type GoalActivity = { type: 'goalActivity', goalActivity: 'goalValue' };

function handleReaction(arg: Status): Status['status']
function handleReaction(arg: GoalActivity): GoalActivity['goalActivity']
function handleReaction(arg: Status | GoalActivity) { 
    if ('status' in arg) {
        return arg.status;
    } else {
        return arg.goalActivity;
    }
}

const a = handleReaction({ type: 'status', status: 'statusValue' })
//    ^? 'statusValue'
const b = handleReaction({ type: 'goalActivity', goalActivity: 'goalValue' })
//    ^? 'goalValue'

Playground

Upvotes: 0

tmhao2005
tmhao2005

Reputation: 17504

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>;
    }
}

Upvotes: 4

Related Questions