Search code examples
typescriptunion-types

How to map 2 union types into a JavaScript Map object?


From the following Union type:

type Modifier =
  | Date
  | RangeModifier
  | BeforeModifier
  | AfterModifier
  | BeforeAfterModifier
  | DaysOfWeekModifier
  | FunctionModifier
  | undefined;

...I've built the following out of the type names:

const MODIFIER_NAMES = [
    'undefined',
    'Date',
    'RangeModifier',
    'BeforeModifier',
    'AfterModifier',
    'BeforeAfterModifier',
    'DaysOfWeekModifier',
    'FunctionModifier',
] as const;
type ModifierNamesTuple = typeof MODIFIER_NAMES;
type ModifierNames = ModifierNamesTuple[ number ];

I need to strictly map the names in ModifierNames to their corresponding type in Modifier, so that I can use those in a JS Map object. Something like...

const modifierMap = new Map<ModifierNames, Modifier>();

...but with the intended type safety between the key and its value. For example:

// For these modifiers (notice their type)...
const rangeModifier: Modifier = {
    from: new Date(),
    to: new Date()
}
const beforeModifier: Modifier = {
    before: new Date()
}
// The following should be invalid
modifierMap.set('BeforeModifier', rangeModifier);

// While the following should be valid
modifierMap.set('RangeModifier', rangeModifier);

How can I achieve this?


Solution

  • type FunctionModifier = {
      tag: 'FunctionModifier'
    }
    type DaysOfWeekModifier = {
      tag: 'DaysOfWeekModifier'
    }
    
    type Dictionary = {
      'Date': Date,
      'DaysOfWeekModifier': DaysOfWeekModifier,
      'FunctionModifier': FunctionModifier,
      'undefined': undefined
    }
    
    // credits goes to https://stackoverflow.com/a/50375286
    type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
      k: infer I
    ) => void
      ? I
      : never;
    
    type Values<T> = T[keyof T]
    
    /**
     * First step
     */
    type HashMap =
      {
        [Prop in keyof Dictionary]: Map<Prop, Dictionary[Prop]>
      }
    
    /**
     * Second step
     */
    type UnionOfStates = Values<HashMap>
    
    /**
     * Third step
     */
    type MapOverloading = UnionToIntersection<UnionOfStates>
    
    const modifierMap: MapOverloading = new Map();
    
    modifierMap.get('FunctionModifier') // FunctionModifier | undefined
    modifierMap.set('DaysOfWeekModifier', { tag: 'DaysOfWeekModifier' }) // ok
    
    modifierMap.set('DaysOfWeekModifier', { tag: 'invalid' }) // error
    modifierMap.set('DaysOfWeekModifier', 42) // error
    
    

    Playground

    In order to make it work you need to overload modifierMap. I mean, you need to create a union of all possible Map states and then intersect them

    UPDATE If you want to call map.set inside a function, you need then to infer function arguments

    type FunctionModifier = {
      tag: 'FunctionModifier'
    }
    type DaysOfWeekModifier = {
      tag: 'DaysOfWeekModifier'
    }
    
    type Dictionary = {
      'Date': Date,
      'DaysOfWeekModifier': DaysOfWeekModifier,
      'FunctionModifier': FunctionModifier,
      'undefined': undefined
    }
    
    type Values<T> = T[keyof T]
    
    const modifierMap = new Map<keyof Dictionary, Values<Dictionary>>();
    
    
    const setter = <Key extends keyof Dictionary>(key: Key, value: Dictionary[Key]) => {
      modifierMap.set(key, value);
    }
    
    setter('Date', new Date) // ok
    setter('Date', 32)// error