Skip to main content

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 types with & (an intersection type).

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">
info

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​

  1. Pull in HTML attributes with ComponentProps (do not hand-write them)
  2. Auto-generate variant types with VariantProps (keep them in sync with the definition)
  3. Use clear type names (ButtonProps, not Props)
  4. export the 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 (including ref) at once
  • VariantProps auto-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.