DEV Community

Conditional Return Types: How to Return the Right Type

Chris Cook on March 16, 2023

Conditional return types are a powerful feature of TypeScript that allow you to specify different return types for a function based on the type of ...
Collapse
 
joelbonetr profile image
JoelBonetR πŸ₯‡ β€’

While it's somewhat funny to see hacks with TS Types I've never found use-cases for this thingies in real life.

I'd rather do:

const concat = (str1: string, str2: string) => str1.concat('', str2);

const sum = (num1: number, num2: number) => num1 + num2;
Enter fullscreen mode Exit fullscreen mode

Which make each more reusable, less prone to error and overall more readable.

I've let a like, tho 😁 cheers!

Collapse
 
zirkelc profile image
Chris Cook β€’ β€’ Edited

To be honest, my example was rather abstract in the context of JavaScript, since there is no logic of operator overloading like in other programming languages like C#.

I was just looking for an illustrative example that could make sense from a purely logical point of view, and I ended up with this example :D

Nevertheless, I prepared another example that makes more sense in daily life. It is a serialization function that converts an object into different types (String, Uin8tArray, Stream) depending on the generic type parameter TFormat. The return type is derived depending on the value of TFormat:

type JsonFormat = { type: "json" };
type BinaryFormat = { type: "binary" };
type StreamFormat = { type: "stream" };
type Format = JsonFormat | BinaryFormat | StreamFormat;

type SerializeReturnType<TFormat extends Format> = 
  TFormat extends JsonFormat 
  ? string
  : TFormat extends BinaryFormat
  ? Uint8Array
  : TFormat extends StreamFormat
  ? ReadableStream<Uint8Array>
  : never;

function serialize<TFormat extends Format>(
  obj: Record<string, unknown>,
  format: TFormat,
): SerializeReturnType<TFormat> {

  if (format.type === "json") {
    const jsonString = JSON.stringify(obj);
    return jsonString as SerializeReturnType<TFormat>;
  }

  if (format.type === "binary") {
    const textEncoder = new TextEncoder();
    const uint8array = textEncoder.encode(JSON.stringify(obj));
    return uint8array as SerializeReturnType<TFormat>;
  }

  if (format.type === "stream") {
    const textEncoder = new TextEncoder();
    const uint8array = textEncoder.encode(JSON.stringify(obj));
    const stream = new ReadableStream({
      start(controller) {
        controller.enqueue(uint8array);
        controller.close();
      },
    });

    return stream as SerializeReturnType<TFormat>;
  }

  throw new Error("Invalid format");
}

const s1 = serialize({ a: 1, b: 2 }, { type: "json"});    // return type is string
const s2 = serialize({ a: 1, b: 2 }, { type: "binary"});  // return type is Uint8Array
const s3 = serialize({ a: 1, b: 2 }, { type: "stream"});  // return type is Stream
Enter fullscreen mode Exit fullscreen mode

TypeScript playground

Thank for your like! :)

Collapse
 
joelbonetr profile image
JoelBonetR πŸ₯‡ β€’

Anytime! 😁

This one looks amazing, I believe it's pretty clear what the intended message was with this example, thank you πŸŽ–οΈ

Collapse
 
balazssoltesz profile image
Balazs Soltesz β€’

That is what function overloading is for. tutorialsteacher.com/typescript/fu...

Collapse
 
zirkelc profile image
Chris Cook β€’

Yes, function overloading would also work in this case. I posted about function overloading in my previous post.

However, this post and example should simply illustrate that there are other ways that may be less verbose than overloading a function with multiple signatures.

Collapse
 
vindecodex profile image
Vincent Villaluna β€’

No need for conditionals:

const fn = <T>(a: T, b: T): T => returnValue as T;
Enter fullscreen mode Exit fullscreen mode

usage:

const num = fn<number>(1, 2);
const str = fn<string>('1', '2');
const custom = fn<CustomType>(valA, valB);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
vindecodex profile image
Vincent Villaluna β€’

Doing conditionals with types makes the function more dependent with types, the more Types the more if blocks you will add into your function

Collapse
 
alansikora profile image
Alan Sikora β€’

Type 'string' is not assignable to type 'T extends string ? string : number'

Why I'm I getting this error?

Image description

Is there as specific config I need?

Collapse
 
zirkelc profile image
Chris Cook β€’

I had the same issue. It seems that the generic parameter T causes a circular reference when it is used as function type parameter and return type.

You can refer to my second example that fixes this issue: Playground

Collapse
 
alansikora profile image
Alan Sikora β€’

That works Chris, thank you very much for the quick reply!

Thread Thread
 
zirkelc profile image
Chris Cook β€’

Sure, glad I could help :)

Collapse
 
nyngwang profile image
Ning Wang β€’ β€’ Edited

Apologies for raining on your parade, but if your post is about "I made conditional return type work" because of "I solved the cause -- circular reference", then you're wrong.

  1. The key part is that TypeScript simply doesn't unpack that T extends string? string : number for you when you're writing the function body. I'm not sure about the reason for it. This makes either as number or as string cannot satisfy the type T extends string? string : number.
  2. Based on 1., to fix it you just mark those returns as any. Now, you get what you want from the returned object: it's either number or string depending on the argument.
  3. So, why your workaround can work? By extracting the conditional return type into a type parameter R, and explicitly marking the return expression as R, you made the return type of that return the same as that of your function signature: both are R. So no error.

So, like it or not, both cases work with as any, playground. But I found your way a little bit counter-intuitive as you made the return type one of those generic parameters and always let TypeScript fill-out it for you. In that case, it shouldn't be a parameter.

I hope this could help.

Collapse
 
mbhaskar98 profile image
Bhaskar β€’

Nice to know information.

Interestingly for the original example Example compiler doesn't give any if return type is something like this - return true as R;.

Check out here - Playground

Looks like compiler allowing anything to pass.

For these second example compiler gives error - Serialization, probably due to type being inferred with each return statement?

Collapse
 
corners2wall profile image
Corners 2 Wall β€’

Seems as strange hack

Collapse
 
zirkelc profile image
Chris Cook β€’

May I ask why?

Collapse
 
corners2wall profile image
Corners 2 Wall β€’

Not