Search code examples
typescripttype-inferencediscriminated-uniontype-narrowing

Typescript - narrowing a type union from function return types


Why is it that Typescript cannot discriminate a type union whose types are from the return types of function, without explicitly declaring the return type on the function?

Here I do not specify the return values of the event creator functions and the union type cannot be narrowed.

enum EventType {
  FOO = "foo",
  GOO = "goo",
}

function createFooEvent(args: {
  documentId: number | null
}) {
  return {
    type: EventType.FOO,
    data: args
  }
}
function createGooEvent(args: {
  id: number
  isSelected: boolean
}) {
  return {
    type: EventType.GOO,
    data: args
  }
}

type EventArgType =
  | ReturnType<typeof createFooEvent>
  | ReturnType<typeof createGooEvent>

function eventHandler(event: EventArgType) {
  switch(event.type) {
    case EventType.FOO: {
      // Note that `event` contains `data` but `data`'s type is a union and has not been discriminated
      event.data;
      break
    }
  }
}

But if I specify the return types as follows then the union can be discriminated.

function createFooEvent(args: {
  documentId: number | null
}): {
  type: EventType.FOO,
  data: {
    documentId: number | null
}} {
  return {
    type: EventType.FOO,
    data: args
  }
}
function createGooEvent(args: {
  id: number
  isSelected: boolean
}): {
  type: EventType.GOO,
  data: {
    id: number
    isSelected: boolean
}} {
  return {
    type: EventType.GOO,
    data: args
  }
}

Here is an example in TS playground.


Solution

  • Because typescript does not infer the constant as the type by default: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions

    For example:

    var a = 'test'
    

    Typescript will infer type of a as string not as 'test'.

    You can fix it using as const:

    var a = 'test' as const;
    

    In this case a will be of type 'test'.

    Same for your code:

      function createFooEvent(args: {
        documentId: number | null
      }) {
        return {
          type: EventType.FOO,
          data: args
        };
      }
    

    The return type of the function is {type: EventType} instead of {type:'foo'}.

    Adding as const to the return type, will work as you expect TS Playground

    function exampleOne(){
      enum EventType {
        FOO = "foo",
        GOO = "goo",
      }
    
      function createFooEvent(args: {
        documentId: number | null
      }) {
        return {
          type: EventType.FOO,
          data: args
        } as const;
      }
      function createGooEvent(args: {
        id: number
        isSelected: boolean
      }) {
        return {
          type: EventType.GOO,
          data: args
        } as const;
      }
    
      type EventArgType =
        | ReturnType<typeof createFooEvent>
        | ReturnType<typeof createGooEvent>
    
      function eventHandler(event: EventArgType) {
        switch(event.type) {
          case EventType.FOO: {
            // event.data in this case will be {documentId: number|null}
            event.data;
            break
          }
        }
      }
    }