Mitsunee

Typing Function Components in React with TypeScript

One of TypeScript's biggest strengths is providing autocompletion and typechecking in IDEs. Properly typing components makes writing JSX a lot easier and can avoid many common errors. The @types/react package provides many utility types on the "react" import (and React namespace) that make typing your components easy to integrate with HTML attributes, props of other components and typechecking styles and CSS Custom Properties.

What is a component?

In React a function component is any function that takes a single parameter - an object containing properties - and returns either ReactNodes or null. If your IDE displays the inferred return type you will often see Element or Element | null.

To attach a type you can either use React.FC (or import type { FC } from "react" if the React namespace is not available globally) or simply attach your Props type directly.

const Columns: React.FC<MyProps> = function ({ children }) {
  return <div className="columns">{children}</div>;
};

// or

function Columns({ children }: MyProps) {
  return <div className="columns">{children}</div>;
}

The children property

The children property represents the equivalent to a Component's innerHTML. React provides the type PropsWithChildren an object type which contains all valid types in an optional children property.

interface MyProps extends React.PropsWithChildren {
  someProp: string;
}

// or

type MyProps = { someProp: string } & React.PropsWithChildren;
// Note I'll stop showing both interface and type now, you can always use whichever you prefer or even use Union Types.

If your Component requires children to be present you can make yourself a utility type based on PropsWithChildren and use it in your props type:

type RequiredChild = Exclude<
  React.PropsWithChildren["children"],
  undefined | boolean | null | Iterable<any>
>;

type RequiredChildren = RequiredChild | RequiredChild[];

interface MyProps {
  children: RequiredChildren;
  someProp: string;
}

Note the lack of ? at children: RequiredChildren. As a rule of thumb all properties that do not have one must be explicitly set in JSX, even if undefined is an allowed value.

Building a Type representing an HTML Element

For building Types representing HTML Elements React again provides as a helpful type: ComponentProps. This type takes a generic argument with a string of which HTML Element you want to represent and gives you the corresponding type, (which includes children where appropriate!) representing this Element.

const StyledLink: React.FC<React.ComponentProps<"a">> = ({
  children,
  ...props
}) => {
  return (
    <a {...props} className="link-style">
      {children}
    </a>
  );
};

You can also use Pick and Omit to further narrow the types you want to be available on your Component or replace properties with different types:

interface StyledLinkProps extends Omit<React.ComponentProps<"a">, "className"> {
  className?: string[] | string;
}

const StyledLink: React.FC<StyledLinkProps> = ({
  children,
  className = [], // won't be undefined now
  ...props
}) => {
  const classNames: string = ["link-style", className].flat().join(" ");

  return (
    <a {...props} className={classNames}>
      {children}
    </a>
  );
};

A Note on spread in JSX

In the above examples {...props} is intentionally the first property/properties in JSX. As with objects the order of spread and keys matters. Putting {...props} after certain properties will in theory allow props to override your values, which may or may not be a feature you want. If you do not want this behaviour always spread first, then set the other properties.

A note on the Generic Argument

If you would like to build a utility type based on ComponentProps you will need to use the following type (as string is not narrow enough):

import type { Class } from "classcat";

export type CC = { className?: Class };
type HTMLTag = keyof JSX.IntrinsicElements;
export type HTMLPropsCC<T extends HTMLTag> = CC &
  Omit<React.ComponentProps<T>, "className">;

If you would like your utility type to also allow passing other React components you can change the generic to also allow extending React.JSXElementConstructor<any>.

Representing Props of other Components

ComponentProps also allows for accessing the props of another component. You can use this to build a utility type to extend a picked selection of properties from other components:

type PickProp<
  C extends React.JSXElementConstructor<any>,
  P extends keyof React.ComponentProps<C>
> = Pick<React.ComponentProps<C>, P>;

// Example:
interface CloseButtonProps extends PickProp<typeof Button, "onClick"> {
  // includes onClick from Button
}

// same as:
interface CloseButtonProps {
  onClick?: React.ComponentProps<typeof Button>["onClick"];
}

Note: You can also combine this with Omit or Partial or use different property names on each component. Use Exclude to turn optional properties into required ones:

interface CloseButtonProps {
  handleClose: Exclude<
    React.ComponentProps<typeof Button>["onClick"],
    undefined
  >;
}

Properties to represent CSS Custom Properties

Using the CSSProperties type there are two approaches to using Custom Properties: The as keyword to override typechecks or extending the type. Generally it is recommend to extend the CSSProperties with your variables to let TypeScript properly typecheck everything. If you are sure that you are only setting values to string you can simply use as React.CSSProperties on the object declaration. Here is an example with proper typechecking on a required and an optional CSS Custom Property:

interface ColoredLinkProps extends React.PropsWithChildren {
  href: string;
  hover: string;
  color?: string; // This one's optional!
}

interface ColoredLinkStyles extends React.CSSProperties {
  "--hover": string;
  "--color"?: string;
}

function ColoredLink({ children, href, hover, color }: ColoredLinkProps) {
  const style: ColoredLinkStyles = {
    "--hover": hover
  };
  // handle optional properties with if
  if (color) style["--color"] = color;

  return (
    <a href={href} className="fancy-link" style={style}>
      {children}
    </a>
  );
}

Preact

Since Preact started out as a direct React replacement many of its types behave the same. Here are some of the above types modified to work with Preact:

import type { Class } from "classcat";
import type {
  ComponentChild,
  ComponentChildren,
  ComponentProps,
  ComponentType
} from "preact";
import type { JSX } from "preact/jsx-runtime";

// children
type RequiredChild = Exclude<ComponentChild, null | undefined | false>;
export type RequiredChildren = RequiredChild | RequiredChild[];
export type PropsWithChildren = { children?: ComponentChildren }; // preact doesn't seem to have this

// ComponentProps on HTML Tags
export type CC = { className?: Class };
type HTMLTag = keyof JSX.IntrinsicElements;
export type HTMLProps<T extends HTMLTag> = ComponentProps<T>;
export type HTMLPropsCC<T extends HTMLTag> = CC &
  Omit<HTMLProps<T>, "className">;

// ComponentProps on Components
type PickProp<
  C extends ComponentType<any>,
  P extends keyof ComponentProps<C>
> = Pick<ComponentProps<C>, P>;

Also note that CSSProperties is on the JSX namespace, not Preact's! You can easily export it like this if you prefer:

import type { JSX } from "preact/jsx-runtime";

export type CSSProperties = JSX.CSSProperties;

Published:
Last Edited:
Permalink

Tags

Similar Posts

Koyanskaya vs Oberon - Who to roll for?

With the arrival of the 6th Anniversary as well as Lostbelt 6 many players are unsure whether to prioritise Koyanskaya or Oberon. Should you roll for the enabler of the Buster Reloading meta Koyanskaya of Light or the NP Damage support Oberon?