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 the arguments. This can be useful when you want to enforce type safety and ensure that the return type matches the expected type.
For example, consider a function for a custom plus operator with two arguments. If the arguments are strings, the two strings are concatenated and returned. If the arguments are numbers, it adds the two numbers together and returns the sum.
function plus<T extends string | number>(a: T, b: T): T extends string ? string : number {
if (typeof a === 'string' && typeof b === 'string') {
return (a + b) as string;
}
if (typeof a === 'number' && typeof b === 'number') {
return (a + b) as number;
}
throw new Error('Both arguments must be of the same type');
}
const result1 = plus(1, 2); // result1 has type number
const result2 = plus('Hello ', 'World'); // result2 has type string
In this code, the plus
function takes two arguments of type T
, which can be either a string
or a number
. The function then uses a conditional return type to specify that the return type should be a string if T extends string
, and a number otherwise.
However, TypeScript has trouble correctly inferring the return type within the function implementation. The compiler reports errors on lines 3 and 7, although the return type is correctly inferred on lines 14 and 15 when the function is called.
The problem is that the type T
is used in both the function signature and the conditional return type, which can lead to a circular reference error. To fix this, we need to use a separate type parameter R
for the return type:
function plus<T extends string | number, R = T extends string ? string : number>(a: T, b: T): R {
if (typeof a === 'string' && typeof b === 'string') {
return (a + b) as R;
}
if (typeof a === 'number' && typeof b === 'number') {
return (a + b) as R;
}
throw new Error('Both arguments must be of the same type');
}
const result1 = plus(1, 2); // result1 has type number
const result2 = plus('Hello ', 'World'); // result2 has type string
In this example, the R
type parameter is used to specify the return type based on the conditional type. This avoids the circular reference error and allows the function to be correctly typed.
I hope you found this post helpful. If you have any questions or comments, feel free to leave them below. If you'd like to connect with me, you can find me on LinkedIn or GitHub. Thanks for reading!
Top comments (16)
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:
Which make each more reusable, less prone to error and overall more readable.
I've let a like, tho π cheers!
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 ofTFormat
:TypeScript playground
Thank for your like! :)
Anytime! π
This one looks amazing, I believe it's pretty clear what the intended message was with this example, thank you ποΈ
That is what function overloading is for. tutorialsteacher.com/typescript/fu...
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.
No need for conditionals:
usage:
Doing conditionals with types makes the function more dependent with types, the more Types the more if blocks you will add into your function
Type 'string' is not assignable to type 'T extends string ? string : number'
Why I'm I getting this error?
Is there as specific config I need?
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
That works Chris, thank you very much for the quick reply!
Sure, glad I could help :)
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.
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 eitheras number
oras string
cannot satisfy the typeT extends string? string : number
.return
sas any
. Now, you get what you want from the returned object: it's eithernumber
orstring
depending on the argument.R
, and explicitly marking the return expressionas R
, you made the return type of thatreturn
the same as that of your function signature: both areR
. 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.
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?
Seems as strange hack
May I ask why?
Not