'How should I design with React/Typescript: Server sends list of steps. Each step has a type and payload

I'm looking for some good ideas on software design for this problem:

A server sends an array of Steps:

type StepTypeEnum = 'SECURITY' | 'PERSONAL_DETAILS' | ... ;
type Step = { stepType: StepTypeEnum, payload: ??? };
StepsController.GET(): Array<Step>;

The client receives the array of steps and renders each step using React.

Each step has a different rendering component and payload type. For example:

const Security = ({ username, password }) => <span>...</span>;
const PersonalDetails = ({ firstName, surName }) => <span>...</span>;
// etc...

What's the best way to implement it with typing? The first ideas that comes to mind is the Factory pattern, but I'm not sure how to deal with the facts that payload can be of any type. Should I just type it as any and downcast it in the component?

For example, what I kind of have now:

const Factory = step => ({
  SECURITY: ({payload}) => <Security username={payload.username} password={payload.password} />,
  PERSONAL_DETAILS = ({payload}) => <PersonalDetails firstName={payload.firstName} ... />
}[step.type])

const Component = Factory(step);
<Component payload={payload} />

This forces me to use any to describe the payload type



Solution 1:[1]

This is a fairly common design pattern. What you are doing is essential the same as what's described in this redux section on type checking actions and action creators.

What you want to do is have a type for each specific pairing of stepType and payload. You then define a generalized Step type as a union of all the valid pairings. Now when you check if the stepType is 'SECURITY', typescript will know that the payload type can only be the type which goes with a security step. (Make sure you don't destructure before checking or this won't work).

We'll define the props/payload interfaces first because we can use the same interfaces for our steps as for our render components.

interface SecurityProps {
    username: string;
    password: string;
}

interface PersonalDetailsProps {
    firstName: string;
    surName: string;
}

const Security = ({ username, password }: SecurityProps) => 
    <span>{username}</span>;

const PersonalDetails = ({ firstName, surName }: PersonalDetailsProps) => 
    <span>{`${firstName} ${surName}`}</span>;

Now we create a union of steps. We can define them individually and then combine, but I'm going to write and combine them in one step. We can get your StepTypeEnum by looking at the stepTypes in our union, if you need this.

type Step = {
    stepType: 'SECURITY';
    payload: SecurityProps;
} | {
    stepType: 'PERSONAL_DETAILS';
    payload: PersonalDetailsProps;
}

type StepTypeEnum = Step['stepType'];

We need a component that can render any Step. A map like what you have is fine but I'm going to use a switch instead as no extra typing is needed. Our union type does all the heavy lifting.

const RenderStep = ({step}: {step: Step}) => {
    switch (step.stepType) {
        case 'SECURITY':
            return <Security {...step.payload} />; // knows that payload is SecurityProps
        case 'PERSONAL_DETAILS':
            return <PersonalDetails {...step.payload} />; // knows that payload is PersonalDetailsProps
        default:
            return null;
    }
}

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 Linda Paiste