Home > OS >  TypeScript generics - expect type argument to be enum, and refer to its keys
TypeScript generics - expect type argument to be enum, and refer to its keys

Time:10-21

I am trying to create a reusable multistep wizard component in react and typecript. The wizard would require certain values to be passed to it through a react context object. The value of that context object must follow a general pattern:

export interface WizardContextValues {
  currentStep: number;
  setStep: (stepState: number) => void;
  completedSteps: { [key: string]: boolean };
  setCompletedSteps: (state: { [key: string]: boolean }) => void;
  disabledSteps: { [key: string]: boolean };
}

You can see that for completedSteps and disabledSteps, I am expecting an object. But I'd like to constrain this a bit more. Let's say for a specific wizard, I have an enum of steps:

export enum UploadWizardSteps {
  UPLOAD_FILE,
  GIVE_DETAILS,
  CONFIRMATION
}

I would like to actually make WizardContextValues generic, such that it takes the steps enum as an argument. Something like this:

export interface WizardContextValues<Steps> {
  currentStep: number;
  setStep: (stepState: number) => void;
  completedSteps: { [key in Steps]: boolean };
  setCompletedSteps: (state: { [key in Steps]: boolean }) => void;
  disabledSteps: { [key in Steps]: boolean };
}

type UploadWizardContext = WizardContextValues<UploadWizardSteps>

I get an error when trying to use key in Steps, saying Type 'Steps' is not assignable to type 'string | number | symbol'. Type 'Steps' is not assignable to type 'symbol'

This sort of makes sense, as when defining the generic interface WizardContextValues<Steps>, typescript has no idea that Steps is an enum and that its keys can be referenced using the key in operator.

typescript playground showing the issue

How can I create this generic type such that certain properties of UploadWizardContext must be objects whose keys are values of UploadWizardSteps?

CodePudding user response:

The way this is currently written, TypeScript has no way of knowing anything about Steps when it's used inside WizardContextValues. You could presumably pass anything in there as the type, including things that can't be used as keys.

You can solve this by restricting the types that can be used for Steps using extends.

By default, TypeScript enums use number values, though you could use the wider string | number | symbol type to fully reflect what's usable for object properties:

export enum UploadWizardSteps {
  UPLOAD_FILE,
  GIVE_DETAILS,
  CONFIRMATION
}

export interface WizardContextValues<Steps extends string | number | symbol> {
  currentStep: number;
  setStep: (stepState: number) => void;
  completedSteps: { [key in Steps]: boolean };
  setCompletedSteps: (state: { [key in Steps]: boolean }) => void;
  disabledSteps: { [key in Steps]: boolean };
}

type UploadWizardContext = WizardContextValues<UploadWizardSteps>

TypeScript playground

  • Related