Lukor
Lukor

Reputation: 1707

Typescript return type depending on parameter

I am trying to write a function which takes a parameter of type boolean and returns one of two types, depending on the value of the input. I have found two approaches:

function dependsOnParameter<B extends boolean>(x: B): B extends true ? number : string {
    if (x) {
        return 3;
    } else {
        return "string";
    }
}

Here, TypeScript says that Type '3'/'"string"' is not assignable to type 'B extends true ? number : string'.

My other approach looks like this:

function dependsOnParameter(x: true): number;
function dependsOnParameter(x: false): string;
function dependsOnParameter(x: boolean): number | string {
    if (x) {
        return 3;
    } else {
        return "string";
    }
}

This compiles; however, if I try to use my function:

function calling(x: boolean) {
    dependsOnParameter(x);
}

I get Argument of type 'boolean' is not assignable to parameter of type 'false'.

Is there any way to achieve what I want without using any?

Upvotes: 98

Views: 69395

Answers (5)

hhk
hhk

Reputation: 139

whenever i use a boolean type, because it almost aways you can just not set it, which result in undefined. i always give it a default value of false

function test<T extends boolean = false>(isOn?:T);

if you call test(); it will assume the type is false, instead of undefined.

btw typescript doesn't handle well undefine type for some reason. which gave me alot of headache in the past

which you can see in this example

 async function ke() {
  const k = await HaiV2();

  k.usage.hello();
}

async function test1() {
  return {
    hello() {
      return "yeeepii";
    },
  };
}

async function HaiV2<NoLimit extends boolean | undefined>(
  noLimit?: NoLimit
): Promise<{ usage: NoLimit extends true ? {} : { hello(): string } }> {
  return {
    usage: noLimit === true ? {} : await test1(),
  };
}

Upvotes: 0

Phil Kang
Phil Kang

Reputation: 1018

I faced a similar issue, except that the dictating boolean parameter is optional and defaults to false. In this case, the conditional type approach described by @Titian Cernicova-Dragomir breaks down (as @AndyO observed).

There are 8 possible cases to consider:

  1. Passing true
  2. Passing false
  3. Passing undefined
  4. Omitting the parameter altogether
  5. Passing boolean (= true | false)
  6. Passing true | undefined
  7. Passing false | undefined
  8. Passing boolean | undefined

I failed to produce a single function declaration that reliably covers all of them. However, 2 declarations are sufficient; one with a parameter (which we use in our conditional check), and one without.

function foo(): string
function foo<B extends boolean | undefined>(x: B): B extends true ? number : string

function foo(x?: boolean): number | string {
  return x ? 123 : 'asdf'
}

function calling(
  bool: boolean,
  boolOrUndefined: boolean | undefined,
  trueOrUndefined: true | undefined,
  falseOrUndefined: false | undefined,
) {
  foo(true)             // number
  foo(false)            // string
  foo()                 // string
  foo(undefined)        // string
  foo(bool)             // string | number
  foo(boolOrUndefined)  // string | number
  foo(trueOrUndefined)  // string | number
  foo(falseOrUndefined) // string
}

Things become complicated when we use an option object, as the number of cases grow exponentially. It's better to make three declarations--one for the truthy case, one for the falsy case, and one for the catch-all:

function bar(x: { y: true }): number
function bar(x?: { y?: false }): string
function bar(x?: { y?: boolean }): number | string

function bar(x?: { y?: boolean }): number | string {
  return x?.y ? 123 : 'asdf'
}

function calling2(
  bool: boolean,
  boolOrUndefined: boolean | undefined,
  trueOrUndefined: true | undefined,
  falseOrUndefined: false | undefined,
  z: undefined | { y: false } | { y: undefined },
  w: { y?: true }
) {
  bar()                         // string
  bar(undefined)                // string
  bar({})                       // string
  bar({ y: undefined })         // string
  bar({ y: true })              // number
  bar({ y: false })             // string
  bar({ y: bool })              // string | number
  bar({ y: boolOrUndefined })   // string | number
  bar({ y: trueOrUndefined })   // string | number
  bar({ y: falseOrUndefined })  // string
  bar(z)                        // string
  bar(w)                        // string | number
}

Upvotes: 5

axellbrendow
axellbrendow

Reputation: 295

This is one way:

function dependsOnParameter<B extends boolean>(x: B): B extends true ? number : string {
    return (x === true ? 3 : "string") as B extends true ? number : string;
}

Here, the condition itself (B extends true ? number : string) is considered as a type. This type is called a Conditional Type.

Upvotes: 7

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249466

Both approaches are valid. If your function uses conditional types in the return it will need to use type assertions, as typescript will not try to reason about the conditional type since it contains a free type parameter:

function dependsOnParameter<B extends boolean>(x: B): B extends true ? number : string {
    if (x) {
        return 3 as any;
    } else {
        return "string"as any;
    }
}

This approach uses any which you want to avoid.

The second approach we can get to work without resorting to type assertions by just duplicating the last signature:

function dependsOnParameter(x: true): number;
function dependsOnParameter(x: false): string;
function dependsOnParameter(x: boolean): number | string
function dependsOnParameter(x: boolean): number | string {
    if (x) {
        return 3;
    } else {
        return "string";
    }
}

function calling(x: boolean) {
    dependsOnParameter(x); // returns number| string
    dependsOnParameter(true); // returns number
    dependsOnParameter(false); // returns string
}

The last signature is the implementation signature and is not publicly accessible. You can make it accessible by duplicating it. The compiler is not smart enough to combine the two overloads with true/false and decide the return type is string|number

Edit

We can also combine the two approaches for fewer signatures:

function dependsOnParameter<B extends boolean>(x: B): B extends true ? number : string 
function dependsOnParameter(x: boolean): number | string{
    if (x) {
        return 3;
    } else {
        return "string";
    }
}

Upvotes: 118

Nurbol Alpysbayev
Nurbol Alpysbayev

Reputation: 21851

You can write it like this

function dependsOnParameter<B extends boolean, C = B extends true ? number : string>(x: B): C {
    if (x) {
        return 3 as unknown as C;
    } else {
        return "string" as unknown as C;
    }
}

Upvotes: -4

Related Questions