Repository navigation
Use conditional type to narrow return type of function with a union-typed argument #24929
Description
Activity
- changed the title
[-]Use conditional type for return type to narrow function with union parameter type [/-][+]Use conditional type to narrow return type of function with a union-typed argument[/+]on Jun 13, 2018 More features along these lines are really important to me, too.
Reacted by zernie, C. T. Lin, Miles B Huff, Jonathan Stiansen, Dustin Kane, YQuan, TOPKAT, Steven Luscher, smaspe and GulgDuplicate of #22735 it seems. Nonetheless, I agree strongly with OP
Would love to see this supported. Until then, there are two workarounds you can use:
1. use an
as anycast/assertionfunction foo<T extends string | number>(val: T): T extends string ? string : number { // requires the use of 'as any' type assertion ☹ // easily removed when tsserver learns to understand return val as any; } foo('').charCodeAt(0); // ✔ No Error (as expected) foo('').toExponential(2); // ✔ Error (as expected) foo(3).toExponential(2); // ✔ No Error (as expected) foo(3).charCodeAt(0); // ✔ Error (as expected)
2. function overloads
function foo(val: string): string; function foo(val: number): number; function foo<T extends string | number>(val: T): T { return val; } foo('').charCodeAt(0); // ✔ No Error (as expected) foo('').toExponential(2); // ✔ Error (as expected) foo(3).toExponential(2); // ✔ No Error (as expected) foo(3).charCodeAt(0); // ✔ Error (as expected)
Reacted by Aneil Mallavarapu, Alex Damian, Mikhail Zhuravlev, James Tsang, Alber, lary, Hengtai, Akash Babu , geonmo.nine, keatkeat87 and 44 moreReacted by Akash Babu , Trufa, keatkeat87, Oleg Valter, Makan Taghizadeh, Aathil, David Matos salvador, Piyush Lodaya and Nathan Lyle BlackReacted by Akash Babu , Trufa, keatkeat87, Makan Taghizadeh, Max Henash, David Matos salvador and Nathan Lyle BlackReacted by Akash Babu , Sohail Alam, Trufa, Luis Pais, Harsha Nalluru, keatkeat87, Makan Taghizadeh, aosq, Max Henash, Jaroslav Louma and 4 moreReacted by Alber, jackyzhang, Trufa, Luis Pais, Nonoroazoro, Onur Iltan, keatkeat87, Makan Taghizadeh, Morgan Erlich, Max Henash and 4 moreaneilbaboo commented
on Jun 13, 2018 AuthorMore actionsPatrick Lienau (@rozzzly) That is quite helpful, thank you.
Note that function overloads are not particularly better here, for typesafety. There is absolutely no checking that your overloads are accurate: only that they could be accurate, based on the primary type signature. Conditionally typing the primary signature might help with that, haven't tested it that thoroughly.
Also note that, at least in my experience, which overload Typescript will choose can be very surprising and (apparently, from the user's perspective) inconsistent.
Due to these issues, our project has strongly encouraged developers to avoid using overloads at all. They are occasionally the least-bad option, as they may be here, but they are rarely ever a good option. They almost-always are a work-around for some limitation in the type system, but it's not always obvious that's what they are, which means casting may be considered superior (because at least it's obvious and honest about what it's doing).
Reacted by Patrick Lienau, Ryan Cavanaugh, Aneil Mallavarapu, James Tsang, Jed, Leon, Tony Gentilcore, C. T. Lin, Oleg Valter, Morgan Erlich and 6 more- addedDuplicateAn existing issue was already createdAn existing issue was already created
on Aug 7, 2018 RyanCavanaugh commented
on Aug 7, 2018 MemberMore actionsDuplicate #22735
The core problem here is that type guard operate on values (not type parameters), and you can't actually make meaningful proofs about type parameters because a) they're not manifest at runtime so anything you do is really quite suspect and b) there might be more than one type guard active against expressions matching a type parameter at a time anyway! For example:
function fn<T extends string | number | boolean>(x1: T, x2: T): T extends string ? "s" : T extends number ? "n" : "b" { if (typeof x1 === 'string' && typeof x2 === 'number') { // ... is this OK? return 's'; } }
Reacted by Aneil Mallavarapu, James Tsang, Alex Rao, Christian Boehlke, Yuan Ruo and tellokReacted by smaspeThat wouldn't be OK, because
Thas to bestring | numberthere andstring | numberdoes notextends string. It also failsextends number, so it should return"b". Which makes no sense, but then the function and its return value were poorly chosen—it should probably include an explicitT extends boolean ? "b" : neveron there, and then throw in that case, since apparently the assumption is thatTwill be exactly one of those types for both parameters.There are a lot of limitations like this where TS doesn't do anything because it cannot make guarantees in the general case, but there are a lot of interesting and useful special cases where guarantees would be possible. Another one that springs to mind immediately is TS not recognizing when unions are mutually exclusive and leveraging that for making guarantees it could not if the unions were not mutually exclusive—note that this doesn't really have anything to do with the
^operator proposal, because even if we had it, TS doesn't use that information as it might. I have no idea what kind of effort would be involved in leveraging them, or what the priority on them is or should be, but I hope the TS team does consider them important and at least vaguely hopes to someday tackle those kinds of problems, rather than sweeping them all under the "we can't make guarantees in the general case" rug.Reacted by Aneil Mallavarapu, Peter Caisse, Jed, Bartek, C. T. Lin and smaspeHere is another example that fails, in the context of code for a project of mine:
I believe this is the same issue at play. (aside: not sure why the typescript playground is ignoring the undefined type I use in it lol. but the point stands)
I expected this example to work, but it didn't:
type Options = 'yes' | 'no'; const op = <T extends Options>(value: T): T extends 'yes' ? 'good' : 'bad' => { if (value === 'no') { return 'bad'; } return 'good'; };Instead, there are two errors:
Type '"bad"' is not assignable to type 'T extends "yes" ? "good" : "bad"'.andType '"good"' is not assignable to type 'T extends "yes" ? "good" : "bad"'. Not sure what I'm missing here.Works as expected if cast the strings to any.
Reacted by mountainmoon, Anatoly Ostapenko, Duncan Giles, Tim Straubinger, Christian Rackerseder, Eirik, Ivan Wang, Pedro Durek, Nelson Martell, Joram Ruitenschild and 19 morejack-williams commented
on Oct 9, 2018 CollaboratorMore actionsKevin Ryan (@krryan)
The conditional type is distributive, so that wouldn't be the behaviour. The typestring | numberwould distribute so you would get's'for the left, and'n'for the right, giving's' | 'n'const x: 's' | 'n' = fn<string | number>("hello",3); // okLuke (@lukeautry)
One issue is that if you pass a value of typeneverthen the conditional type will evaluate tonever, which neither'bad'or'good'satisfies. You would need to do some work to convince a type-checker things are ok.type Options = 'yes' | 'no'; const op = <T extends Options>(value: T): T extends 'yes' ? 'good' : 'bad' => { if (value === 'yes') return 'good'; if (value === 'no') return 'bad'; return value; // Would need to have value narrow to never, which it does not };
For that to work the compiler really needs to know that
Tcan only be inhabited by two values. But given that fact, you might aswell just write.type Options = 'yes' | 'no'; function op(value: 'yes'): 'good'; function op(value: 'no'): 'bad'; function op(value: Options): 'good' | 'bad' { if (value === 'yes') return 'good'; if (value === 'no') return 'bad'; return value; // never }
The complications that Ryan mentions mean having a general solution that works beyond very simple cases is non-trivial.
Reacted by Ivan Wang and Carles NúñezReacted by smaspetypescript-bot commented
on Dec 15, 2018 ContributorMore actionsThis issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
Reacted by smaspeShould TypeScript Bot (@typescript-bot) be closing a duplicate when the older issue is locked? That doesn't seem healthy.
Reacted by Marin Marinov, bb010g, Paul Souche, Jason Dreyzehner, zen-ventzi-marinov, Mick Dekkers, Jan Peer Stöcklmair, Phil Eberhardt, Claudiu Hojda, Nico Jansen and 42 more3 remaining items
I thought something like this might work but ...
type TestFuncRes = { type1: number; type2: string; }; type Test<T extends keyof TestFuncRes> = { (arg: T): TestFuncRes[T]; }; const testFunc = (arg: keyof TestFuncRes) => { const type1: number = 22; const type2: string = 'test string'; return arg === "type1" ? type1: type2 }; const test: Test<'type1'> = testFunc('type1');
any thoughts?
Reacted by Jacob Page, Vojtech Mašek and Morgan ZolobI have a similar issue to the comment above where I have a helper function, that checks your on the correct product for a feature as well as checking if you have the correct permissions.
import React, { ReactNode } from "react"; import { Product } from "@//platform"; import { CaslEnum } from "@//enums/casl"; import Features from "./features"; import BoundCan from "./"; interface Props { children?: ReactNode; feature: string; I?: CaslEnum; not?: boolean; passThrough?: boolean; } const Can = ({ children, feature, I = CaslEnum.VIEW, not, passThrough }: Props) => { const paths = feature.split(":"); let featureObject = Features; paths.forEach(key => { featureObject = featureObject[key]; }); if (!featureObject.products.includes(Product)) { return null; } if (children) { const boundCanProps = { this: feature, I, not, passThrough, }; return <BoundCan {...boundCanProps}>{children}</BoundCan>; } return !featureObject.permissions.includes(I); }; export default Can;So you can see that it can be used as a function or as a wrapper for a component, this is so we can disable things your allowed to see but not edit and also hide things that aren't features of the product you are on.
My problem is that when using it as a component wrapper
<Can feature="test:feat:help" I={CaslEnum.VIEW}> <button disabled={Can({ feature: "test:feat:help", I: CaslEnum.EDIT })}>help</button> </Can>It throws an error at both the component usage and the function usage
Function usage =
Type 'boolean | Element' is not assignable to type 'boolean | undefined'
Component usage =JSX element type 'boolean | Element | null' is not a constructor function for JSX elements. Type 'false' is not assignable to type 'Element | null'.I want to be able to change the return type definition dependent on whether
childrenexists.I have a very similar use case
const propOrValue = < T extends Record<string, any>, K extends keyof T >( obj: T, key?: K, ): K extends undefined ? T : T[K] => { if (key) { return obj[key]; } return obj; // <- error here };
the possible workaround is use
anyreturn obj as any;
Update: Here is explanation #22735 (comment)
Reacted by theopenroad and Marvin HeilemannReacted by michalpokojskiJack Williams (@jack-williams) the example you gave is not ideal/sound 👇
type Options = 'yes' | 'no'; function op(value: 'yes'): 'good'; function op(value: 'no'): 'bad'; function op(value: Options): 'good' | 'bad' { if (value === 'yes') return 'good'; if (value === 'no') return 'bad'; return value; // never }
Ideally, when I change
if (value === 'no') return 'bad'toif (value === 'no') return 'good',
it should fail type check as it violatesfunction op(value: 'no'): 'bad'.
However, it does not only pass type check but also sets a wrong type on the result.Reacted by smaspeReacted by smaspeReacted by zerofront, Josh and Bryan LeeCould this be revisited?
type MappingOf<T extends 'X' | 'Y'> = T extends 'X' ? '1' : T extends 'Y' ? '2' : never function mappingOf<T extends 'X' | 'Y'>(t : T): MappingOf<T> { switch (t) { case 'X': return '1' // Type '"1"' is not assignable to type 'MappingOf<T>'.ts(2322) case 'Y': return '2' // Type '"2"' is not assignable to type 'MappingOf<T>'.ts(2322) } }
I'd like to express a mapping between two values in types (
MappingOf) so I can express functions on a specific value (e.g. 'X') and get type-safety in the mapping, but I also need a runtime version of the mapping (mappingOf) and I'd like the compiler to force me to do the correct implementation.What's the state of this? Is this being tracked by some other issue? Seems like a pretty common js pattern, but can't find this problem tracked in any open issue?
Looks like it's dead. Too bad. The best thing about this is that ... wait for it ... the TypeScript docs actually give a function signature with a generic conditional return type as an example, but they smartly leave out the implementation ... is it because it cannot really be implemented in current TypeScript, or what? Why show that off as a feature, when you don't provide a working implementation? :D
None of the workarounds we have so far are type-safe, except for having multiple functions (with different names), but that's not really DRY.
This is one of the cases where logically, you'd think something would just work, but in reality it does not. I originally found this thread trying to figure out how I could get rid of function overloads, because they seem to deal with this, but it's at the expense of the function implementation not really being type-safe, and I don't like that. (TypeScript doesn't really make sure that the function implementation adheres to ALL its overloads).
Using a generic conditional was my second bet, but sadly ... it also is unusable. Makes one wonder why the syntax is even allowed for function return types :D
Reacted by Volker Hartmann, Max, Sander Siim, Peter Oberrauner, Alex Holachek, Hristijan Stevanoski, Hans Fredric Waadeland, karlismelderis, Hayden, Dmitry and 25 moreDoes the following example also fall into this category?
type In1 = 'a' | 'b'; type Out1<T extends In1> = T extends 'a' ? {type: 'a'; a: true} : T extends 'b' ? {type: 'b'; b: true} : never; function func1<T extends In1>(type: T): Out1<T> { switch (type) { case 'a': // Type '{ type: T; a: true; }' is not assignable to type 'Out1<T>' return {type, a: true}; case 'b': // Type '{ type: T; b: true; }' is not assignable to type 'Out1<T>' return {type, b: true}; default: unreachable(type); } }
It is a pattern continuously used throughout our project and I can't see anything wrong with it. Mapping like this seems like a fundamental feature of TypeScript to me, so I was confused that it didn't work.
Is there a workaround so I can resolve this easily without going intoEdit: It seems that casting the return value toanyterritory?as Out1<T>is sufficient as a workaround.Can we reopen this or is that particular problem tracked in another issue?
Reacted by tmm and MathiasRyanCavanaugh commented
on Jun 14, 2021 MemberMore actionsThe general idea of narrowing on generics is tracked at #33014
Reacted by Lenny, Ronald Martin, Justin Dalrymple and Eero RuoholaReacted by Morgan Erlich, Jakob Brünker and Steven LuscherReacted by LennyCan we reopen this though? I lost like 2 hours on this issue going back and reading the docs because I thought I must be missing something (would a PR adding a note in the docs get merged?).
Reacted by Rishabh Rao, Taesung, Lim, Stefano, barca-reddit, ForeverHG, Yegor Shulga, Maxime, Jose Luis Leon, spsjvc, wbt and 11 moreI find function overloads to be a cleaner and easier to read approach to specifying conditional return types.
Hannah Schultz (@whschultz) Function overloads provide the absolute bare minimum of type safety: they only check that the overloaded types could be assigned to the “real” signature, not that the underlying code actually does what it claims with respect to them. The hope for conditional types was that they would provide a greater guarantee within the body of the function, but because of these issues and the need to cast them, they don’t.
Reacted by Morgan Erlich, Mares, mikeevstropov, theomessin, Malthe Borch and smaspe- use an
as anycast/assertion
this causes another problem, TS will not report wrong return values
function foo<T extends string|number>(value: T): T extends string ? string: number { return {error: 'not string or number'} as any; }it allowed returning an object which is a non-expecting
- function overloads
also, this one causes a problem when you need to return a different type than
Ttype OutputType<T extends string | number> = T extends string? MyInterface: AnotherInterface function getValue<T extends string | number>(val: T): OutputType<T>{ return typeof val==='string'? {type: 'myInterface'} : {type: 'otherInterface'} }Reacted by smaspe- use an
any other ways for conditional return type except overloads, here is function implementation signature
interface A { bar: string; } interface B1 { bat: string; } interface B2 { bat: number; } type TWrapper<T> = { bala: T; }; async function foo<T>(p0: A & B1): TWrapper<T>; async function foo<T>(p0: A & B2): T | null { ... };the
extendsconstraint forp0not working- locked as resolved and limited conversation to collaborators
on Oct 21, 2025

Search Terms
conditional return type narrowing generics
Suggestion
It seems like it should be possible to constrain the return type of a function using a conditional expression:
For example,
Use Cases
A common use case is a function which can process single objects or arrays of objects, and returns single objects or an array, respectively.
Examples
Checklist
My suggestion meets these guidelines: