The switch statements does not enforce exhaustive pattern matching (objective), the syntax is incredibly ugly (subjective) and is a poorly designed language feature (arguably objective). Thankfully, we can eliminate the shortcomings of the switch statement and improve the quality of our code using pattern matching instead.
There is a TC39 Stage 1 pattern matching proposal for the curious types here.
Example 1 (simple):
Say you have a string literal union type:
type Icon = "chart" | "file-check" | "paper-plane" | "users";
and you want to conditionally render the appropriate icon so your first approach is the old trusted switch statement:
const renderIcon = (icon: Icon): JSX.Element => {
switch (icon) {
case "chart":
return <ChartIcon />;
case "file-check":
return <FileCheckIcon />;
case "paper-plane":
return <PaperPlaneIcon />;
case "users":
return <UsersIcon />;
}
};
Here, the function's explicit return type is our only saving grace so to speak — not the switch statement itself. If we were to instead use the implicit return type of JSX.Element | undefined
and then forget a case, the TypeScript compiler would not yell at us. I would therefore argue as a developer I have failed my current task as well as preventing those behind me from introducing bugs because I did communicate my intent clearly — this function needs to account for all possibilities, return a JSX.Element
and have zero side effects.
We can refactor this using a library called ts-pattern to achieve that intent.
const renderIcon = (icon: Icon): JSX.Element => {
return match(icon)
.with("chart", () => <ChartIcon />)
.with("file-check", () => <FileCheckIcon />)
.with("paper-plane", () => <PaperPlaneIcon />)
.with("users", () => <UsersIcon />)
.exhaustive();
};
Example 2 (advanced):
Say you have a branded type / discriminate union type / tagged union type:
type V1Report = {
id: string;
results: { /* whatever */ },
year: string;
}
type V2Report = {
id: string;
results: { /* whatever */ },
year: string;
}
type Report = { tag: "v1", value: V1Report } | { tag: "v2", value: V2Report }
// later in some function
match(report)
.with({ tag: "v1" }, (v1Report) => /* do something with report */)
.with({ tag: "v2" }, (v2Report) => /* do something with report */)
.exhaustive()
Top comments (0)