'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 |