Add TypeScript To An Existing React Project
Learn how to add TypeScript to your existing React project in a few simple steps.
React's props model is extremely powerful. One of its most useful features is the ability to pass a component as a prop. This lets you create composable pieces of UI, helping to make your components more reusable.
The trouble is that this can often be difficult to type correctly. Let's fix that.
One of the most flexible ways to pass a component as a prop is to get the component to receive JSX. Let's look at the example below:
tsx
interfaceLayoutProps {nav :React .ReactNode ;children :React .ReactNode ;}constLayout = (props :LayoutProps ) => {return (<><nav >{props .nav }</nav ><main >{props .children }</main ></>);};<Layout nav ={<h1 >My Site</h1 >}><div >Hello!</div ></Layout >;
Here, we're passing <h1>My Site</h1>
to the nav
prop, and <div>Hello!</div>
to the children
prop.
We're typing our props as React.ReactNode
, which is a type that accepts any valid JSX. Note that we're not using React.ReactElement
or JSX.Element
. I cover why in this article.
The second method is, instead of passing in JSX as a prop, we pass in an entire component as a prop.
Some definitions here. JSX is the thing a component returns. <Wrapper />
is JSX. Wrapper
is the component
.
The simplest way to type this in TypeScript is by using React.ComponentType
:
tsx
constRow = (props : {icon :React .ComponentType <{className ?: string;}>;}) => {return (<div ><props .icon className ="h-8 w-8" /></div >);};<Row icon ={UserIcon } />;
Here, we're typing the icon
prop as React.ComponentType
. We're passing { className?: string }
to React.ComponentType
, indicating that this is a component that can receive a className
prop.
This basically says icon
can be any component that can receive a className
prop. This is a very flexible type, and it's easy to use.
Using React.ElementType
lets you pass a native tag as a prop OR a custom component.
tsx
constRow = (props : {element :React .ElementType <{className ?: string;}>;}) => {return (<div ><props .element className ="h-8 w-8" /></div >);};<Row element ={"div"} />;<Row element ={UserIcon } />;
This is an extremely flexible definition and, again, very easy to use. We'll even get autocomplete on all the options we can pass to element
.
For more information about React.ComponentType
and React.ElementType
, check out this exercise in my Advanced React course.
The final method is to be able to receive any component and infer its props. This is very flexible but also extremely complex to type.
In my Advanced React and TypeScript course, I devote half of an entire section to this topic.
The final solution I landed on is documented here.
tsx
import {ComponentPropsWithRef ,ElementType ,ForwardedRef ,forwardRef ,useRef ,} from "react";typeFixedForwardRef = <T ,P = {}>(render : (props :P ,ref :React .Ref <T >) =>React .ReactNode ) => (props :P &React .RefAttributes <T >) =>React .ReactNode ;constfixedForwardRef =forwardRef asFixedForwardRef ;typeDistributiveOmit <T ,TOmitted extendsPropertyKey > =T extends any ?Omit <T ,TOmitted > : never;export constUnwrappedAnyComponent = <TAs extendsElementType >(props : {as ?:TAs ;} &DistributiveOmit <ComponentPropsWithRef <ElementType extendsTAs ? "a" :TAs >,"as">,ref :ForwardedRef <any>) => {const {as :Comp = "a", ...rest } =props ;return <Comp {...rest }ref ={ref }></Comp >;};// Can be passed 'as' prop but defaults to 'a'constAnyComponent =fixedForwardRef (UnwrappedAnyComponent );// Defaulted to 'a'<AnyComponent href ="/" />;// It's now a div, so can't be an href!<Type '{ as: "div"; href: string; }' is not assignable to type 'IntrinsicAttributes & { as?: "div" | undefined; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ...; }, "as"> & RefAttributes<...>'. Property 'href' does not exist on type 'IntrinsicAttributes & { as?: "div" | undefined; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ...; }, "as"> & RefAttributes<...>'. Did you mean 'ref'?2322Type '{ as: "div"; href: string; }' is not assignable to type 'IntrinsicAttributes & { as?: "div" | undefined; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ...; }, "as"> & RefAttributes<...>'. Property 'href' does not exist on type 'IntrinsicAttributes & { as?: "div" | undefined; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ...; }, "as"> & RefAttributes<...>'. Did you mean 'ref'?AnyComponent as ="div"="/" />; href
If you're in a situation where you can choose either of the above approaches, I would lean towards passing JSX as a prop.
It's not only easy to type (React.ReactNode
) but also very performance-friendly. JSX passed to a component as a prop is not re-rendered when that parent component re-renders. This can be a huge performance boost.
But if you do need the other methods, then React.ElementType
and React.ComponentType
are both easy to type and easy to use.
If you can, stay away from using the open-ended 'as' prop. But if you do need it, then the description in my advanced course will help.
Share this article with your friends
Learn how to add TypeScript to your existing React project in a few simple steps.
Learn the essential TypeScript configuration options and create a concise tsconfig.json file for your projects with this helpful cheatsheet.
Big projects like Svelte and Drizzle are not abandoning TypeScript, despite some recent claims.
Learn about TypeScript performance and how it affects code type-checking speed, autocomplete, and build times in your editor.
When typing React props in a TypeScript app, using interfaces is recommended, especially when dealing with complex intersections of props.
Learn TypeScript in 2023 with this step-by-step guide. Understand the basics, essential types, unions, and narrowing techniques.