Search code examples
typescriptunion-typesgeneric-constraintstype-narrowing

Type Narrowing not working as expected with generic constraints


I have a situation where the generic type is constraint by a union type, but I find that doing this does not make the type narrowing to work as expected. The code snippet below shows what is going on.

function somefunc<T extends string | number>(input: T): T {
  if (typeof input === "string") {
    // expecting input to be of type "string"
    // but input becomes of type T & "string"
    input
  } else {
    // expecting input to be of type "number"
    // but input becomes of type T extends string | number
    input
 }
}

If I do away with the generics and just annotate the function argument as string | number it works, but for my use case I need to have the generic constraints.

Edit

The use case is basically an attempt to also use this with conditional types. Basically I want to have a result type be a conditional type that depends on the input type. So when the input type is number, the result is also number, when input is string result becomes string also. Basically this:

type Result<T> = T extends string ? string : number

function somefunc<T extends string | number>(input: T): Result<T> {
  if (typeof input === "string") {
    // expecting input to be of type "string"
    // but input becomes of type T & "string"
    input
  } else {
    // expecting input to be of type "number"
    // but input becomes of type T extends string | number
    input
 }
}
 

I am probably missing something, but the question is, how do I have union based generic constraint and have type narrowing work as I expect. In the above code, that will mean, in the if branch, input becomes type string while in the else branch, it becomes number (or at least it becomes T & number)

** Edit **

I was able to achieve want I wanted using function overloading. I was only wondering if same thing can be achieved using generics and conditional types.


Solution

  • The reason why it's not narrowed is explained in this answer

    Hacky way to narrow the type correctly:

    type Result<T> = T extends string ? string : number;
    
    function somefunc<T extends string | number>(input: T): Result<T> {
      const inputNarrowed: string | number = input;
    
      if (typeof inputNarrowed === "string") {
        inputNarrowed; // string
      } else {
        inputNarrowed; // number
      }
    
      return inputNarrowed as Result<T>;
    }
    

    Alternative solution (which I prefer) with overload + conditional generic

    type Result<T> = T extends string ? string : number;
    
    function somefunc<T extends string | number>(input: T): Result<T>;
    function somefunc(input: string | number) {
      if (typeof input === "string") {
        input; // string
      } else {
        input; // number
      }
    
      return input;
    }
    
    const str = somefunc("string"); // string
    const num = somefunc(1); // number