Skip to main content

match

Exhaustive variant selection: map a discriminant to a class value where a missing case is a compile error. Needs no build integration.

Every case covered

The lookup is a plain object literal: its keys are the values the discriminant can take, and each one holds the class value that case should produce. Nothing is prefixed or assembled — the helper only chooses between classes you already wrote.

A size variant
function Button({ size }: { size: "sm" | "md" | "lg" }) {  const sizing = match(size, {    sm: "px-2 py-1 text-sm",    md: "px-3 py-2 text-base",    lg: "px-4 py-3 text-lg",   // omit one and it won't compile  });}

A missing case does not compile

This is the whole point of the helper. Widen the union — add a size, rename a tone — and the lookup stops type-checking until you handle the new case, instead of returning nothing and leaving the element unstyled.

Extra cases are allowed, and that matters more than it sounds. TypeScript narrows a const size: "sm" | "lg" = "sm" down to "sm" at the call site, so a lookup parameter typed as a full record of the union would reject the lgentry as an excess property. Inferring the object's own type keeps the superset legal while still demanding every case be covered.

The runtime fallback

A third argument is returned when the discriminant is not one of the keys at runtime — the case TypeScript cannot rule out, because the value arrived from outside the type.

Fallback
match(tone, { primary: "bg-blue-600", danger: "bg-red-600" }, "bg-gray-200");// unknown tone at runtime → the fallback

Use it wherever the discriminant comes from an API response, a URL parameter or a CMS field. Where the value is a union you control, leave the fallback off and let the exhaustiveness check do the work.

No build integration

Every class in a match lookup is already a literal in your source, so Tailwind finds it on its own scan. match builds no prefix at runtime and needs no build integration — with cn, it is one of the two helpers that work whether or not the plugin is installed.

That makes it the standard workaround for a class the tailess scannercannot see — a value held in a variable, an interpolated template, a computed key. Move the decision into a lookup and the full class is back in the source, where Tailwind's own scan reaches it without any help.

Reaching the scanner
const size = match(scale, { sm: "text-sm", lg: "text-2xl" });

The result is a plain class string, so it composes with everything else: ss and cn both take it as an argument.