Add TypeScript To An Existing React Project
Learn how to add TypeScript to your existing React project in a few simple steps.
Since 4.1, TypeScript has had the power to manipulate and transform strings using template literal syntax. Take the example below:
typescript
type InternalRoute = `/${string}`const goToRoute = (route: InternalRoute) => {}
You'll be able to call goToRoute
using anything starting with a /
. But any other string will be an error.
You can use unions inside template literal types to expand into larger unions:
typescript
type EntityAttributes = `${'post' | 'user'}${'Id' | 'Name'}`// ^? 'postId' | 'userId' | 'postName' | 'userName'
You can even use infer
inside template literals.
typescript
type GetLastName<TFullName extends string> =TFullName extends `${infer TFirstName} ${infer TLastName}` ? TLastName : never
Here, ${infer TFirstName} ${infer TLastName}
represents any two strings with a space between:
text
Matt PocockJimi HendrixCharles BarkleyEmmylou Harris
And it instantiates TFirstName
and TLastName
as type variables which can be used if it matches the string passed in. The ? TLastName
returns the last name, meaning you can use GetLastName
like so:
typescript
type Pocock = GetLastName<'Matt Pocock'>// ^? "Pocock"type Hendrix = GetLastName<'Jimi Hendrix'>// ^? "Hendrix"type Barkley = GetLastName<'Charles Barkley'>// ^? "Barkley"
What about more advanced use cases? What if we wanted to replace the space in the name with a dash?
typescript
type ReplaceSpaceWithDash<TFullName extends string> =TFullName extends `${infer TFirstName} ${infer TLastName}`? `${TFirstName}-${TLastName}`: nevertype Name = ReplaceSpaceWithDash<'Emmylou Harris'>// ^? "Emmylou-Harris"
Very nice - we just change the result to ${TFirstName}-${TLastName}
. Now, our type variables seem a bit misnamed. Let's switch:
TFullName
to TString
TFirstName
to TPrefix
TLastName
to TSuffix
typescript
type ReplaceSpaceWithDash<TString extends string> =TString extends `${infer TPrefix} ${infer TSuffix}`? `${TPrefix}-${TSuffix}`: never
Now it's more generic. But not quite generic enough - let's make this type helper be able to handle replacing any string with another string.
typescript
type Replace<TString extends string,TToReplace extends string,TReplacement extends string,> = TString extends `${infer TPrefix}${TToReplace}${infer TSuffix}`? `${TPrefix}${TReplacement}${TSuffix}`: never
We've swapped out the with TToReplace
, and -
with TReplacement
. This ends up working pretty well:
typescript
type DashName = Replace<'Matt Pocock', ' ', '-'>// ^? "Matt-Pocock"
Except, there are a couple of bugs. For instance, that never
looks a bit suspicious. If Replace
doesn't find any TToReplace
, it returns never
:
typescript
type Result = Replace<'Matt', ' ', '-'>// ^? never
What is the correct behaviour? We want to just return whatever string got passed in:
typescript
type Replace<TString extends string,TToReplace extends string,TReplacement extends string,> = TString extends `${infer TPrefix}${TToReplace}${infer TSuffix}`? `${TPrefix}${TReplacement}${TSuffix}`: TStringtype Result = Replace<'Matt', ' ', '-'>// ^? "Matt"
The second bug is that it only replaces once. If there's more than one instance of the TToReplace
, it ignores the second onwards.
typescript
type DashCaseName = Replace<'Matt Pocock III', ' ', '-'>// ^? "Matt-Pocock III"
This feels like a tricky bug to fix - until we consider how ${infer TPrefix}${TToReplace}${infer TSuffix}
works. In a string like Matt Pocock III
, it will infer like so:
TPrefix
: "Matt"TSuffix
: "Pocock III"This means that the rest of the work needs to be performed on TSuffix
. Again, this feels intractable - until we realise that you can call types recursively. This means that we can wrap TSuffix
in StringReplace
:
typescript
type StringReplace<TString extends string,TToReplace extends string,TReplacement extends string,> = TString extends `${infer TPrefix}${TToReplace}${infer TSuffix}`? `${TPrefix}${TReplacement}${StringReplace<TSuffix,TToReplace,TReplacement>}`: TStringtype Result = StringReplace<'Matt Pocock III', ' ', '-'>// ^? "Matt-Pocock-III"
Whenever you're doing recursion, you need to make sure you don't end up in an infinite loop. So let's track what StringReplace
gets passed:
First, StringReplace<"Matt Pocock III", " ", "-">
. This returns Pocock III
.
Second, StringReplace<"Pocock III", " ", "-">
. This returns III
.
Finally, StringReplace<"III", " ", "-">
. Since it can't find any instances of " "
, it just returns TString
(in this case, "III"
). We found the end of our recursive loop!
Thanks for following along on this deep dive of conditional types, template literals and infer
. If you enjoyed it, you'll love my full course where we go even deeper, and build up all the knowledge with step-by-step exercises.
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.