cbdeveloper
cbdeveloper

Reputation: 31335

Typescript function return type based on optional parameter presence without using function overloads

Typescript playground

My goal here is to return a different type based on the presence of a optional condition: "CONDITION" parameter. And I'm trying to accomplish this without resorting to overloads.

type TYPE_1 = "TYPE_1"
type TYPE_2 = "TYPE_2"

type CONDITION = "CONDITION"

function foo(condition?: CONDITION): TYPE_1 | TYPE_2 {
    if (condition) {
        return "TYPE_1";
    }
    else {
        return "TYPE_2";
    }
}

const shouldBeType_1 = foo("CONDITION");    // ERROR: THIS IS BEING EVALUATED AS UNION TYPE: "TYPE_1" | "TYPE_2"
const shouldBeType_2 = foo();               // ERROR: THIS IS BEING EVALUATED AS UNION TYPE: "TYPE_1" | "TYPE_2"

This is easy to accomplish with overloads:

/* ########################################### */
/* #### THIS IS EASY TO DO WITH OVERLOADS #### */
/* ########################################### */

function foo_overloaded(): TYPE_2
function foo_overloaded(condition: "CONDITION"): TYPE_1
function foo_overloaded(condition?: "CONDITION"): TYPE_1 | TYPE_2 {
    if (condition) {
        return "TYPE_1";
    }
    else {
        return "TYPE_2";
    }
}

const overloaded_shouldBeType_1 = foo_overloaded("CONDITION");   // SUCCESS: THIS IS TYPE_1
const overloaded_shouldBeType_2 = foo_overloaded();              // SUCCESS: THIS IS TYPE_2

What is the proper way of doing it without overloads? Or am I over complicating it and overloading is simply the way to go in this situation?

Also there is this question here on SO: TypeScript: function return type based on argument, without overloading

It suggests that an interface should be used as a map to the return type, like:

interface Registry {
    A: number,
    B: string,
    C: boolean
}

function createType<K extends keyof Registry>(type: K, value: Registry[K]): Registry[K] {
    return value;
}

But I can't do that, because condition is either "CONDITION" | undefined. So how can I map the undefined type? I also tried doing that with conditional type. Something like:

type RETURN_TYPE<T extends undefined | "CONDITION"> = T extends "CONDITION" ? TYPE_1 : TYPE_2;

But that didn't work either.

Upvotes: 8

Views: 3999

Answers (1)

Martin
Martin

Reputation: 2273

I would say you would go with overloaded functions in this case, you can solve this partly with the following:

function foo<T extends CONDITION | undefined>(condition?: T): T extends CONDITION ? TYPE_1 : TYPE_2 {
    if (condition) {
        //`as any` is intentional here: https://stackoverflow.com/questions/55641731/typescript-conditional-type-complains-type-not-assignable
        return "TYPE_1" as any;
    } else {
        return "TYPE_2" as any;
    }
}

With this, the following works fine:

const shouldBeType_1 = foo("CONDITION") // It is TYPE_1;

But when you don't pass any params it would not work:

const shouldBeType_2 = foo(); // It is TYPE_1 | TYPE_2

Of course if you pass undefined directly, it works correctly:

const shouldBeType_2 = foo(undefined); // It is TYPE_2;

So long stroy short, right now, the cleanest approach for your problem, is to use function overloading.


EDIT As it has been pointed out if I add a default parameter for the generic type, this would work with omitted param as well.

function foo<T extends CONDITION | undefined = undefined>(condition?: T): T extends CONDITION ? TYPE_1 : TYPE_2 {
    if (condition) {
        return "TYPE_1" as any;
    } else {
        return "TYPE_2" as any;
    }
}

// Both work:
const shouldBeType_1 = foo("CONDITION") // It is TYPE_1;
const shouldBeType_2 = foo(); // It is TYPE_2;

Upvotes: 11

Related Questions