'Typescript dynamic type based on the parent functions parameter enum value

I am trying to conditionally define a child functions parameter type based on a enum variable obtained from the parent functions parameter value.

It seems that when a function parameter value is used to conditionally define a child functions parameter type, typescript is unable to understand the condition.

See example:

enum PaymentMethodTypes {
  CARD = "card",
  DIRECT_DEBIT = "direct_debit",
}

type Card = {
  type: PaymentMethodTypes.CARD
}

type DirectDebit = {
  type: PaymentMethodTypes.DIRECT_DEBIT
}

const Component = ({ paymentType }: {paymentType: PaymentMethodTypes}) => { 
  type PaymentMethod = typeof paymentType extends PaymentMethodTypes.CARD ? Card : DirectDebit;

  const method = (paymentMethod: PaymentMethod) => {
    console.info(paymentMethod.type)
    if (paymentType === PaymentMethodTypes.CARD) {
      console.info(paymentMethod.type === PaymentMethodTypes.CARD);
      // ERROR: This condition will always return 'false' since the types 'PaymentMethodTypes.DIRECT_DEBIT' and 'PaymentMethodTypes.CARD' have no overlap.
    }
  }
}

When a variable is defined, rather than via the function parameter or a conditional value, Typescript is able to understand the type condition correctly.

See example:

enum PaymentMethodTypes {
  CARD = "card",
  DIRECT_DEBIT = "direct_debit",
}

type Card = {
  type: PaymentMethodTypes.CARD
}

type DirectDebit = {
  type: PaymentMethodTypes.DIRECT_DEBIT
}

const paymentType = PaymentMethodTypes.Card;

const Component = () => { 
  type PaymentMethod = typeof paymentType extends PaymentMethodTypes.CARD ? Card : DirectDebit;

  const method = (paymentMethod: PaymentMethod) => {
    console.info(paymentMethod.type)
    if (paymentType === PaymentMethodTypes.CARD) {
      console.info(paymentMethod.type === PaymentMethodTypes.CARD);
      // This now works
    }
  }
}

Any suggestions to resolve this would be greatly appreciated!



Solution 1:[1]

You'll have to use a generic parameter to obtain the narrowed type of paymentType.

See TS Playground for why this is

const ComponentFix = <T extends PaymentMethodTypes>({ paymentType }: {paymentType: T}) => { 
  type PaymentMethod = T extends PaymentMethodTypes.CARD ? Card : DirectDebit;

  const method = (paymentMethod: PaymentMethod) => {
    console.info(paymentMethod.type)
    if (paymentType === PaymentMethodTypes.CARD) {
      console.info(paymentMethod.type === PaymentMethodTypes.CARD);
    }
  }
}

In short the typeof operator returns the union of all possible values, which means it will actually be evaluating

type PaymentMethod = PaymentMethodTypes.CARD | PaymentMethodTypes.DirectDebit extends PaymentMethodTypes.CARD ? Card : DirectDebit;

Which always fails, hence it defaults to DirectDebit

EDIT: From comments discussion, The TS engine isn't able to infer type discrimination across function boundaries, instead you should seek to purify your function, see on playground

const ComponentFix = <T extends PaymentMethodTypes>({ paymentType }: {paymentType: T}) => { 
  type PaymentMethod = T extends PaymentMethodTypes.CARD ? Card : DirectDebit;

  const method = (paymentMethod: PaymentMethod) => {
    console.info(paymentMethod.type)
    if (paymentMethod.type === PaymentMethodTypes.CARD) {
      console.info(paymentMethod.type === PaymentMethodTypes.CARD);
    }
  }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1