Search code examples
typescripttypestype-inferenceconditional-typesgeneric-function

Why is there no type-error when assigning a conditional return-type of string to a number?


I want to define a typescript function with a conditional return-type that depends on the type of the argument: Called with an argument of type true it returns a string and called with an argument of type false it returns a number. I came up with the following function foo() which almost works as expected:

  • calling it with true the correct return-type (string) is inferred
  • calling it with false the correct return-type (number) is inferred

But when directly assigning the function call to an incompatible type, no type error is thrown, why is that?

// generic function with conditional return-type TReturn that depends on TSomeArg
const foo = <
  TSomeArg extends boolean,
  TReturn = TSomeArg extends true ? string : number,
>(
  someArg: TSomeArg,
): TReturn => {
  // to return the conditional type TReturn, one needs to explictly assert it, since typescript will otherwise infer the union type: 'Some String'|1234
  return (someArg ? "Some String" : 1234) as unknown as TReturn;
};

// this works fine
const testOne = foo(true); // inferred string as expected: "const testOne: string"
//      ^?

const testTwo = foo(false); // inferred number as expected: "const testTwo: number"
//      ^?

const testThree: number = testOne; // throws Error as expected: "Type 'string' is not assignable to type 'number'."
const testFour: string = testTwo; // throws Error as expected: "Type 'number' is not assignable to type 'string'."

// BUT, here I would expect some type-errors
const testFive: number = foo(true); // unexpected: no type-error (string is inccorrectly accepted as a number)
const testSix: string = foo(false); // unexpected: no type-error (number is incorrectly accepted as a string)

// only when I explicitly define TSomeArg, then the type-errors are thrown as expected
const testSeven: number = foo<true>(true); // throws type-error as expected: "Type 'string' is not assignable to type 'number'."
const testEighth: string = foo<false>(false); // throws type-error as expected: "Type 'number' is not assignable to type 'string'."

same code at ts-playground


Solution

  • As written there doesn't seem to be any reason why foo is generic in TReturn, since the best case scenario is that TReturn is specified as TSomeArg extends true ? string : number, and any other type argument will give you behavior you don't want. But the question here isn't about avoiding the bad behavior, but to understand why it's happening.


    The main cause is that function return types serve as inference targets, as implemented in microsoft/TypeScript#16072. When you write

    const testFive: number = foo(true);
    

    you are calling foo(true) in a context whereby the return type is expected to be number. And this serves as an inference site for TReturn. Generic type argument defaults are only used if inference fails when you call the function. But inference succeeds, and so the above call is equivalent to

    const testFive: number = foo<true, number>(true);
    

    and you get the behavior you don't like.