Search code examples
typescriptenumsuniquetypescript-generics

TypeScript exhaustive array that contains all properties from a given enum


I have an enum in TypeScript.

I want to write a function that returns an exhaustive array that contains exactly one reference to each property on the enum.

The return of the function will be an Array<{ id: EnumValue, label: string }>.

Sample code:

// I have this enum:
enum StepId {
  STEP_ONE = 'step-one-id',
  STEP_TWO = 'step-two-id',
}


// I want to define some function...
function someFunc() {
  return ...
}


// which returns an Array where every value from `StepId` is represented as an object with that value as the `id` field.
someFunc() === [
  { id: CustomStepIds.STEP_ONE, label: 'Step One', },
  { id: CustomStepIds.STEP_TWO, label: 'Step Two', },
];

I have tried to first solve the problem of enforcing all enum values are represented in an array. I've tried this:

enum CustomStepId {
  STEP_ONE = 'step-one-id',
  STEP_TWO = 'step-two-id',
}

type ValueOf<T> = T[keyof T];

const stepConfig: Array<{ id: ValueOf<typeof CustomStepId>, label: string }> = [
  { id: CustomStepId.STEP_ONE, label: 'Step One', },
  { id: CustomStepId.STEP_TWO, label: 'Step Two', },
  { id: CustomStepId.STEP_TWO, label: 'Step Two', }, // this should error, as STEP_TWO is already defined
];

However as noted in the comment above, while this enforces id is a CustomStepId, it does not enforce uniqueness of the field.


Solution

  • I propose the following, on the basis that I (and much of the Typescript world) eschew Enums in favour of const arrays See typescript playground

    I drafted mechanisms based on a map or a tuple, depending what you prefer or need.

    type MemberOf<Arr extends readonly unknown[]> = Arr[number]
    
    const STEP_IDS = ['step-one-id', 'step-two-id'] as const;
    type StepId = MemberOf<typeof STEP_IDS>;
    
    type StepConfigMap = {[k in StepId]: {
        label:string
    }}
    
    type ConfigTuple<Tuple extends readonly [...unknown[]]> = {
      [Index in keyof Tuple]: {
        id:Tuple[Index],
        label:string
      };
    } & {length: Tuple['length']};
    
    type StepConfigTuple = ConfigTuple<typeof STEP_IDS>;
    
    const CONFIG_MAP: StepConfigMap  = {
        "step-one-id":{
            label:"something"
        },
        "step-two-id":{
            label:"something else"
        },
        // errors as it's already defined
        // An object literal cannot have multiple properties with the same name.(1117)
        "step-two-id":{
            label:"something else"
        }
    } as const;
    
    const CONFIG_TUPLE = [
        {
            id:"step-one-id",
            label:"something",
        },
        {
            id:"step-two-id",
            label:"something",
        },
        // errors as it's already defined
        // Source has 3 element(s) but target allows only 2.
        {
            id:"step-two-id",
            label:"something",
        }
    
    ] as const satisfies StepConfigTuple;