Ethan Lemke
Ethan Lemke

Reputation: 53

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.

Upvotes: 4

Views: 2433

Answers (1)

Andrei Tătar
Andrei Tătar

Reputation: 8295

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
      }
    }
  }
}

Upvotes: 7

Related Questions