class-recipe with TypeScript
A recipe infers the props it accepts from its config:
- An option that a variant does not declare is a type error.
- A variant without a default is required; a variant with a default, or a boolean variant, is optional.
- A boolean variant accepts
true,false,"true", and"false", and an option whose name is a number accepts the number and the string. - A compound variant or a default that names an undeclared variant or
option is a type error, and so is a slot that
slotsdoes not name.
import { cva } from "@lynstack/class-recipe";
const button = cva({ variants: { tone: { neutral: "bg-gray-100", danger: "bg-red-600 text-white" }, size: { sm: "h-8 px-3", md: "h-10 px-4" }, }, defaultVariants: { size: "md" },});
button({ tone: "danger" });
// @ts-expect-error: tone has no default, so it is required.button({ size: "sm" });A config written inline is inferred as it is. Declare a config before the
call as const, so that the options named in its compound and default
variants stay literal types.
Typing component props
Section titled “Typing component props”VariantsOf returns the variants a recipe or slot recipe accepts, without
className or classNames:
import type { VariantsOf } from "@lynstack/class-recipe";
type ButtonVariants = VariantsOf<typeof button>;// => { readonly tone: "neutral" | "danger";// readonly size?: "sm" | "md" | undefined }See Building components for using it in a component.
Reserved names
Section titled “Reserved names”className and classNames are the names of the overrides, so they
cannot be variant names: a config that declares either is a type error.
Variants from a CMS or an API
Section titled “Variants from a CMS or an API”When the classes of a recipe come from outside the code, such as a CMS or
a theme file, type the data with the names of its variants and options.
The recipe then checks every call as above. Write the type with type,
not interface, since an interface does not satisfy the type of
variants:
import { sva } from "@lynstack/class-recipe";import type { SlotClasses } from "@lynstack/class-recipe";
type CardClasses = SlotClasses<"root" | "title">;
type CardTheme = { readonly size: Readonly<Record<"sm" | "md", CardClasses>>; readonly tone: Readonly<Record<"neutral" | "danger", CardClasses>>;};
const theme: CardTheme = await fetchCardTheme();
const card = sva({ slots: ["root", "title"], variants: theme, defaultVariants: { size: "md", tone: "neutral" },});
card({ size: "sm" });
// @ts-expect-error: "lg" is not a size.card({ size: "lg" });If the data is not checked where it arrives, parse it with a schema library whose result has these types, so that a CMS that renames an option fails there instead of adding no classes.
Variant names not known in advance
Section titled “Variant names not known in advance”When the names cannot be known, as in a function that passes on a config
it received, type the variants as RecipeVariants or
SlotRecipeVariants. A recipe then accepts any variant name, with its
option named by a string, and classNames for the declared slots:
import { sva } from "@lynstack/class-recipe";import type { SlotRecipeVariants } from "@lynstack/class-recipe";
function createCard(variants: SlotRecipeVariants) { return sva({ slots: ["root", "title"], variants });}
const card = createCard(theme);
card({ size: "sm", classNames: { title: "font-bold" } });
// @ts-expect-error: an option is named by a string.card({ size: 1 });Such a recipe checks less:
- Every variant is optional, and an undeclared variant or option is not a type error. It adds no classes.
- A slot recipe also accepts classes by slot as an option, such as
size: { root: "p-2" }, because TypeScript cannot leaveclassNamesout of the names that variants may take. Such a value names no option, so the variant uses its default, if it has one. VariantsOfreturnsReadonly<Record<string, string | undefined>>.
Other types
Section titled “Other types”The package exports the types of every config, props object, and recipe,
such as RecipeConfig and SlotRecipeProps, for code that builds on
them; see Exports. For a library of
recipes of other values, build on the types of
@lynstack/recipe.