Skip to content

Use conditional type to narrow return type of function with a union-typed argument #24929

Description

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,

// I want my function to have a different return type depending on input
export function foo<T extends string|number>(
  val: T                                  // input type is a union
): T extends string ? string : number {   // output type is either string or number
  return val;
} 
// expected this to work, but received:
// Type 'T' is not assignable to type 'T extends string ? string : number'.
//  Type 'string | number' is not assignable to type 'T extends string ? string : number'.
//    Type 'string' is not assignable to type 'T extends string ? string : number'.

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

// capitalize a string or each element of an array of strings
function capitalize<T extends string | string[]>(
  input: T
): T extends string[] ? string[] : string {
  if (isString(input)) {
    return input[0].toUpperCase() + input.slice(1);
  } else {
	return input.map(elt => capitalize(elt));
  }
}

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)

Activity

  1. 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
  2. krryan commented on Jun 13, 2018

    @krryan

    More features along these lines are really important to me, too.

  3. AlCalzone commented on Jun 13, 2018

    @AlCalzone
    Contributor

    Duplicate of #22735 it seems. Nonetheless, I agree strongly with OP

  4. rozzzly commented on Jun 13, 2018

    @rozzzly

    Would love to see this supported. Until then, there are two workarounds you can use:

    1. use an as any cast/assertion
    function 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)
  5. aneilbaboo commented on Jun 13, 2018

    @aneilbaboo
    Author

    Patrick Lienau (@rozzzly) That is quite helpful, thank you.

  6. krryan commented on Jun 13, 2018

    @krryan

    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).

  7. RyanCavanaugh commented on Aug 7, 2018

    @RyanCavanaugh
    Member

    Duplicate #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';
        }
    }
  8. krryan commented on Aug 7, 2018

    @krryan

    That wouldn't be OK, because T has to be string | number there and string | number does not extends string. It also fails extends 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 explicit T extends boolean ? "b" : never on there, and then throw in that case, since apparently the assumption is that T will 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.

  9. lukeautry commented on Oct 8, 2018

    @lukeautry

    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"'. and Type '"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.

  10. jack-williams commented on Oct 9, 2018

    @jack-williams
    Collaborator

    Kevin Ryan (@krryan)
    The conditional type is distributive, so that wouldn't be the behaviour. The type string | number would 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); // ok

    Luke (@lukeautry)
    One issue is that if you pass a value of type never then the conditional type will evaluate to never, 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 T can 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.

  11. typescript-bot commented on Dec 15, 2018

    @typescript-bot
    Contributor

    This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  12. aleclarson commented on Dec 15, 2018

    @aleclarson

    Should TypeScript Bot (@typescript-bot) be closing a duplicate when the older issue is locked? That doesn't seem healthy.

  13. 3 remaining items

  14. kurosh-z commented on Apr 6, 2020

    @kurosh-z

    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');

    Screenshot 2020-04-06 at 11 38 16

    any thoughts?

  15. The-Code-Monkey commented on Apr 16, 2020

    @The-Code-Monkey

    I 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 children exists.

  16. tuchk4 commented on May 7, 2020

    @tuchk4

    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 any

    return obj as any;

    Update: Here is explanation #22735 (comment)

  17. ivawzh commented on Jul 25, 2020

    @ivawzh

    Jack 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' to if (value === 'no') return 'good',
    it should fail type check as it violates function op(value: 'no'): 'bad'.
    However, it does not only pass type check but also sets a wrong type on the result.

    Playground Link

  18. NickDarvey commented on Dec 21, 2020

    @NickDarvey

    Could 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.

  19. nagromLobo commented on Apr 5, 2021

    @nagromLobo

    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?

  20. petr-motejlek commented on Apr 14, 2021

    @petr-motejlek

    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

  21. lgrahl commented on Jun 14, 2021

    @lgrahl

    Does 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 into any territory? Edit: It seems that casting the return value to as Out1<T> is sufficient as a workaround.

    Can we reopen this or is that particular problem tracked in another issue?

  22. RyanCavanaugh commented on Jun 14, 2021

    @RyanCavanaugh
    Member

    The general idea of narrowing on generics is tracked at #33014

  23. JGJP commented on Nov 8, 2021

    @JGJP

    Can 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?).

  24. whschultz commented on Mar 14, 2022

    @whschultz

    I find function overloads to be a cleaner and easier to read approach to specifying conditional return types.

    microsoft/TypeScript-Website#1931 (comment)

  25. krryan commented on Mar 15, 2022

    @krryan

    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.

  26. its-dibo commented on Nov 11, 2022

    @its-dibo
    1. use an as any cast/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

    1. function overloads

    also, this one causes a problem when you need to return a different type than T

    type 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'}
    }
    
  27. ciodc commented on Oct 10, 2023

    @ciodc

    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 extends constraint for p0 not working

  28. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions