TypeScript Integration: Type-Safe Components
What you learn in this chapter
You learn the TypeScript types that power the Button component. In React 19, the way you write these types is simpler than before.
Why type safety matters
With types, the "how to use it" of a component is expressed in the code, which helps you prevent mistakes before they happen.
The 4 values that types bring
├─ Editor support: autocompletion / inline documentation
├─ Error prevention: caught at compile time / prevents typos
├─ Safe refactoring: you can see the impact of changes
└─ Self-documentation: usage is clear from the types
The type of the Button's argument
The Button in the final-form chapter wrote the type directly on the argument.
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
// ...
}
The type part joins three pieces with & (an intersection type). Let's look at each one.
When you want to name the type, you can also extract it with type.
type ButtonProps = React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}
function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
// ...
}
① React.ComponentProps<"button">
React.ComponentProps<"button"> is a type that represents all the props an HTML <button> can receive.
// onClick, disabled, type, aria-*, children… and even ref are included
<Button type="submit" disabled={isLoading} onClick={handleClick} aria-label="Submit">
Submit
</Button>
With this, you do not need to define onClick, disabled, and the like one by one yourself.
// ❌ Writing them all by hand is tedious
type ButtonProps = {
onClick?: () => void
disabled?: boolean
type?: "submit" | "reset" | "button"
// …dozens more…
}
// ✅ Pull them all in at once with ComponentProps<"button">
type ButtonProps = React.ComponentProps<"button">
Previously, the type React.ButtonHTMLAttributes<HTMLButtonElement> was commonly used. React.ComponentProps<"button"> includes that and also includes ref, so it is more concise. In React 19, ref can be passed as a normal prop, so this single type fully covers ref support. The latest shadcn/ui uses this one too.
② VariantProps
VariantProps automatically generates the types of variant and size from the cva definition.
import { cva, type VariantProps } from "class-variance-authority"
const buttonVariants = cva("...", {
variants: {
variant: { default: "...", destructive: "...", outline: "..." },
size: { default: "...", sm: "...", lg: "..." },
},
})
type ButtonVariants = VariantProps<typeof buttonVariants>
// => {
// variant?: "default" | "destructive" | "outline" | null | undefined
// size?: "default" | "sm" | "lg" | null | undefined
// }
If you change the definition, the types follow automatically.
// Add ghost / link to cva…
variant: { default: "...", destructive: "...", outline: "...", ghost: "...", link: "..." }
// …and VariantProps updates automatically
// variant?: "default" | "destructive" | "outline" | "ghost" | "link" | null
"The definition and the types never drift apart" is the big advantage of cva + VariantProps.
③ The additional prop
Finally, you add the Button-specific asChild.
{ asChild?: boolean }
Joining these three with & produces the complete type for the Button.
React.ComponentProps<"button"> (button's standard props + ref)
&
VariantProps<typeof buttonVariants> (variant / size from cva)
&
{ asChild?: boolean } (the custom prop)
=
The type of the Button's argument
In React 19, forwardRef generics are unnecessary
Previously, you specified the type of ref with forwardRef's generics.
// React 18 and earlier
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => { ... })
// ↑ ref's type ↑ props' type
In React 19, ref is now passed as a normal prop, and its type is included in React.ComponentProps<"button">, so you just write the type on the function's argument. forwardRef's generics are no longer needed.
// React 19
function Button({ ...props }: React.ComponentProps<"button"> & ...) { ... }
Type-safe usage examples
Autocompletion
<Button variant="" />
// When you trigger completion inside "":
// "default" / "destructive" / "outline" / "secondary" / "ghost" / "link"
Detecting typos and nonexistent values
<Button variant="primry" /> // ❌ a typo for "primary" → type error
<Button size="extra-large" /> // ❌ a nonexistent size → type error
Type checking for ref
const buttonRef = useRef<HTMLButtonElement>(null)
<Button ref={buttonRef}>OK</Button> // ✅
const inputRef = useRef<HTMLInputElement>(null)
<Button ref={inputRef}>NG</Button> // ❌ you cannot pass an input's ref
Slightly more advanced types
You can also use the standard type utilities as needed.
// Prevent className from being passed externally (exclude it with Omit)
type StrictButtonProps = Omit<React.ComponentProps<"button">, "className"> &
VariantProps<typeof buttonVariants>
// Make some props optional (Partial)
type PartialButtonProps = Partial<ButtonProps>
You will rarely need these at first, but it helps to know that "you can make these adjustments too."
Best practices for type definitions
- Pull in HTML attributes with
ComponentProps(do not hand-write them) - Auto-generate variant types with
VariantProps(keep them in sync with the definition) - Use clear type names (
ButtonProps, notProps) exportthe types that consumers use (export type ButtonProps/export { Button, buttonVariants })
Chapter summary
- The Button's type is an intersection of
ComponentProps<"button">+VariantProps+{ asChild } React.ComponentProps<"button">pulls in all of button's props (includingref) at onceVariantPropsauto-generates types from the cva definition and never drifts from it- In React 19,
forwardRef's generics are unnecessary; you just write the type on the argument
In the next chapter, you use everything you have learned so far to build the Button from scratch.