How to use/define Enums with Flow type checking?

13,672

Solution 1

To express an enum with flow you can use $Values utility in conjunction with frozen object type:

export const LOAN_STATUS = Object.freeze({
  PENDING: 'pending',
  CURRENT: 'current',
  DUE: 'due',
  OVERDUE: 'overdue',
  PENDING_PAYMENT: 'pending_payment',
  CHARGED_OFF: 'charged_off',
  VOIDED: 'voided',
  DISPUTED: 'disputed',
  REFUNDED: 'refunded',
  SETTLED: 'settled',
});

type LoanStatus = $Values<typeof LOAN_STATUS>;

export const ACTIVE_LOAN_STATUS: LoanStatus[] = [
  LOAN_STATUS.OVERDUE,
  LOAN_STATUS.CURRENT,
  LOAN_STATUS.DUE,
  LOAN_STATUS.PENDING_PAYMENT,
]

This works starting from 0.60.0 version.

Solution 2

Here is the most concise way to achieve this:

const activeLoanStatuses = {
  current: 'current',
  due: 'due',
  overdue: 'overdue',
  pending_payment: 'pending_payment'
};

const otherLoanStatuses = {
  pending: 'pending',
  charged_off: 'charged_off',
  voided: 'voided',
  disputed: 'disputed',
  refunded: 'refunded',
  settled: 'settled',
};

type ActiveLoanStatus = $Keys<typeof activeLoanStatuses>;
type LoanStatus = $Keys<typeof otherLoanStatuses> | ActiveLoanStatus;

const activeLoanStatusesMap: { [key: LoanStatus]: ?ActiveLoanStatus} = activeLoanStatuses;

if (activeLoanStatusesMap[loan.status]) {

}

Solution 3

While incredibly verbose, and non-scalable, this falls into Flow's "Disjoint Unions" case and such can be implemented using ===. As they mention on that page, Case Analysis is done via that operator, as javascript naturally does with switch-case statements.

In your case, that equates to:

switch(loan.status) {
  'pending':
  'current':
  'due':
  'overdue':
  'pending_payment':
  'charged_off':
  'voided':
  'disputed':
  'refunded':
  'settled':
    // your behavior here
}

As I mentioned, this is highly verbose in code which uses your types, but to counter that, it has the benefit of defining your types without creating a boilerplate object- you simply define your literal options and union them together (your second implementation).

This has the obvious downside of coupling your type definition with your implementations of its consumers, so use with caution.

Share:
13,672

Related videos on Youtube

Chet
Author by

Chet

Updated on June 04, 2022

Comments

  • Chet
    Chet almost 2 years

    I'm trying to migrate an existing codebase to use Flow. Since this project started without Flow, I'm using a pretty typical JS pattern for enums and such.

    Here are a few definitions I want to

    export const LOAN_STATUS  = {
      PENDING: 'pending',
      CURRENT: 'current',
      DUE: 'due',
      OVERDUE: 'overdue',
      PENDING_PAYMENT: 'pending_payment',
      CHARGED_OFF: 'charged_off',
      VOIDED: 'voided',
      DISPUTED: 'disputed',
      REFUNDED: 'refunded',
      SETTLED: 'settled',
    }
    
    export const ACTIVE_LOAN_STATUS = [
      LOAN_STATUS.OVERDUE,
      LOAN_STATUS.CURRENT,
      LOAN_STATUS.DUE,
      LOAN_STATUS.PENDING_PAYMENT,
    ]
    

    Flow works fine until I import this file and it says I need to add type annotations. This seems odd -- why should I have to annotate objects that are entirely static and easily inferred?

    Is there any way that define its type as "static" or "literal"?

    So then I go about thinking how I'm going to add annotations to this. My first thought is just {[key: string]: string} and Array<string>. Flow works, but I'm realizing that these type definitions are totally worthless. So then I try this other approach:

    type LoanStatusValues =
      'pending' |
      'current' |
      'due' |
      'overdue' |
      'pending_payment' |
      'charged_off' |
      'voided' |
      'disputed' |
      'refunded' |
      'settled'
    
    type LoanStatusKeys =
      'PENDING' |
      'CURRENT' |
      'DUE' |
      'OVERDUE' |
      'PENDING_PAYMENT' |
      'CHARGED_OFF' |
      'VOIDED' |
      'DISPUTED' |
      'REFUNDED' |
      'SETTLED'
    
    type ActiveLoanStatus = 
    "current" |
    "due" |
    "overdue" |
    "pending_payment"
    

    And I use the type annotations {[key: LoanStatusKeys]: LoanStatusValues} and Array<ActiveLoanStatus>. But even these annotations loose the fact that this is static!

    It just seems so odd that I'm having to write this much duplicate code. And then if I want to convert just to Flow I can't actually use the types in JS. For example I might do this:

    if (defs.ACTIVE_LOAN_STATUS.indexOf(loan.status) !== -1) {
    
    }
    

    Now if I want to use Flow types, I can't do anything like this:

    type ActiveLoanStatus = 
      "current" |
      "due" |
      "overdue" |
      "pending_payment"
    
    if (loan.status isTypeOf ActiveLoanStatus) {
    
    }
    

    So how am I supposed to use these static enums? I must be doing this wrong!

  • Chet
    Chet over 7 years
    hmm. and if the keys arent equal to the values...?
  • vkurchatkin
    vkurchatkin over 7 years
    You'll have to duplicate in this case, unfortunately
  • Andrew Patton
    Andrew Patton almost 5 years
    From the link to the Utility Types doc: “$Values<T> represents the union type of all the value types (not the values, but their types!) of the enumerable properties.” So, in your example, the LoanStatus type is string, not 'pending' | 'current' | ....
  • Aleksey L.
    Aleksey L. almost 5 years
    @Andrew Patton the trick here is to use frozen object, then value types are resolved as literal types. Give it a try. This part (as many others) is not mentioned in the docs
  • Andrew Patton
    Andrew Patton almost 5 years
    That’s really good to know, thanks for the further explanation! Too bad the docs are so incomplete. I did notice that it only works with frozen objects plus string literal values. If any values are variables (even constants), the type becomes string. For example: const PENDING = 'pending'; export const LOAN_STATUS = Object.freeze({ PENDING, DUE: 'due' });
  • Andrew Patton
    Andrew Patton almost 5 years
    An example of the above from the flow REPL: flow.org/try/…
  • Aleksey L.
    Aleksey L. almost 5 years
    @Andrew Patton Yes, that's because in flow (not like in typescript) consts do not infer literal types, but widen type. To fix this, the type should be specified explicitly (e.g. const PENDING: 'pending' = 'pending';)
  • Constantin M
    Constantin M about 3 years
    Oh yes, thanks a ton guys. Small note it doesn't work on nested object unfortunately. I was trying to make something like javascript const TRACK_EVENTS = Object.freeze({ homepage: { header: 'header_passed', hero: 'hero_passed', } }) tracker = (eventName: $Values<typeof TRACK_EVENTS>) This doesn't seem to be working.