Add TypeScript To An Existing React Project
Learn how to add TypeScript to your existing React project in a few simple steps.
As a frontend developer, your job isn't just pixel-pushing. Most of the complexity in frontend comes from handling all the various states your app can be in.
It might be loading data, waiting for a form to be filled in, or sending a telemetry event - or all three at the same time.
If you aren't handling your states properly, you're likely to come unstuck. And handling states starts with how they're represented in types.
Let's imagine you're building a simple data loader. You might choose to use a type like this to represent its state:
typescript
interface State {status: "loading" | "error" | "success";error?: Error;data?: { id: string };}// Some examples:const example: State = {status: "loading"};const example2: State = {status: "error",error: new Error("Oh no!")};
This seems pretty nice - we can check status
to understand what kind of UI we should display on the screen.
Except - this type lets us declare all sorts of shapes which should be impossible:
typescript
const example3: State = {status: "success",// Where's the data?!};
Here, we're in a success state - which should let us access our data. But it doesn't exist!
typescript
const example4: State = {status: "loading",// We're loading, but we still have an error?!error: new Error("Eek!"),};
And here, we're in a loading state - but there's still an error in our data object!
This is because we've chosen to represent our state using what I call a 'bag of optionals' - an object full of optional properties.
Optional properties are best used when a value might or might not be present. In this case, that isn't right.
status
is loading
, data
or error
are never present.status
is success
, data
is always present.status
is error
, error
is always present.The more accurate way to represent this is using a discriminated union.
Let's start by changing our state to be a union of object, each containing a status.
typescript
type State =| {status: "loading";}| {status: "success";}| {status: "error";};
Now that we've got our scaffolding, we can start adding elements to each branch of the union. Let's re-add our error and data types.
typescript
type State =| {status: "loading";}| {status: "success";data: {id: string;};}| {status: "error";error: Error;};
Now, our examples from above will start erroring.
typescript
// Error: Property 'data' is missingconst example3: State = {status: "success",};const example4: State = {status: "loading",// Error: Object literal may only specify known// properties, and 'error' does not existerror: new Error("Eek!"),};
Our State
type now properly represents all the possible states of the feature. That's a big step forward, but we're not done yet.
Let's imagine we're inside a component in our codebase. We've received our piece of state, and we're looking to use it to render some JSX.
I'll use React here, but this could be any frontend framework.
The first instinct of many developers will be to destructure the elements of State
, but you'll immediately hit errors:
typescript
const Component = () => {const [state, setState] = useState<State>({status: "loading",});const {status,// Error: Property 'data' does not exist on type 'State'.data,// Error: Property 'error' does not exist on type 'State'.error,} = state;};
For many devs, this is going to be tricky to figure out. Both data
and error
can exist on State
, so why am I getting errors?
The reason is that we haven't tried to discriminate the union yet! We don't know which state we're in, so the only properties available are the ones which all the members of the union share. Namely, status
.
Once we've checked which branch of the union we're in, we can safely destructure state
!
typescript
if (state.status === "success") {const { data } = state;}
This strictness is a feature, not a bug. By ensuring you can only access data when the status equals success
, you're encouraged to think of your app in terms of its states, and only access data in the states it's available.
When you start thinking of your app in terms of discriminated states, a lot of things get easier.
Instead of a big optional bag of data, you'll start understanding the connections between data and UI.
Not only that, but you'll be able to think about props in a whole new way.
What if you need to display a component in two slightly different ways? Use a discriminated union:
typescript
type ModalProps =| {variant: "with-description-and-button";buttonText: string;description: string;title: string;}| {variant: "base";title: string;};
Here, buttonText
and description
will only be required when the variant passed in is with-description-and-button
.
Beautiful.
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 different ways to pass a component as a prop in React: passing JSX, using React.ComponentType, and using React.ElementType.
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.