Add TypeScript To An Existing React Project
Learn how to add TypeScript to your existing React project in a few simple steps.
What you might think of as generics in TypeScript is actually three separate concepts:
typescript
// 1. Passing types to typestype PartialUser = Partial<{ id: string; name: string }>;// 2. Passing types to functionsconst stringSet = new Set<string>();// 3. Inferring types from arguments passed to functionsconst objKeys = <T extends object>(obj: T): Array<keyof T> => {return Object.keys(obj) as Array<keyof T>;};const keys = objKeys({ a: 1, b: 2 });// ^? const keys: ("a" | "b")[]
Let's start with passing types to types.
In TypeScript, you can declare a type which represents an object, primitive, function - whatever you want, really.
typescript
export type User = {id: string;name: string;};export type ToString = (input: any) => string;
But let's say you need to create a few types with a similar structure. For instance, a data shape.
The code below isn't very DRY - can we clean it up?
typescript
type ErrorShape = {message: string;code: number;};type GetUserData = {data: {id: string;name: string;};error?: ErrorShape;};type GetPostsData = {data: {id: string;title: string;};error?: ErrorShape;};type GetCommentsData = {data: {id: string;content: string;};error?: ErrorShape;};
If you're OOP-inclined, you could do this using a reusable interface like this:
typescript
interface DataBaseInterface {error?: ErrorShape;}interface GetUserData extends DataBaseInterface {data: {id: string;name: string;};}interface GetPostsData extends DataBaseInterface {data: {id: string;title: string;};}interface GetCommentsData extends DataBaseInterface {data: {id: string;content: string;};}
But it's more concise to create a 'type function', which takes in the type of data and returns the new data shape:
typescript
// Our new type function!type DataShape<TData> = {data: TData;error?: ErrorShape;};type GetUserData = DataShape<{id: string;name: string;}>;type GetPostsData = DataShape<{id: string;title: string;}>;type GetCommentsData = DataShape<{id: string;content: string;}>;
This syntax is important to understand, because it'll come up later!
The angle brackets in <TData>
tell TypeScript that we want to add a type argument to this type. We can name TData
anything, it's just like an argument to a function.
This is a generic type:
typescript
// Generic typetype DataShape<TData> = {data: TData;error?: ErrorShape;};// Passing our generic type// another typetype GetPostsData = DataShape<{id: string;title: string;}>;
Generic types can accept multiple type arguments. You can provide defaults to type arguments, as well as set constraints so only certain types can be passed.
But it's not just types that you can pass types to:
typescript
const createSet = <T>() => {return new Set<T>();};const stringSet = createSet<string>();// ^? const stringSet: Set<string>const numberSet = createSet<number>();// ^? const numberSet: Set<number>
That's right- just like types can accept types as arguments, so can functions.
In this example, we add a <T>
before the parentheses when we declare createSet. We then pass that <T>
manually into Set()
, which itself lets you pass a type argument:
typescript
export const createSet = <T>() => {return new Set<T>();}
That means that when we call it, we can pass a type argument of <string>
to createSet
.
Now we end up with a Set that we can only pass strings to.
typescript
const stringSet = createSet<string>();// Argument of type 'number' is not assignable to parameter of type 'string'.stringSet.add(123);
This is the second thing that people mean when they talk about generics: manually passing types to functions.
You'll have seen this if you use React, and you've needed to pass a type argument to useState
:
typescript
const [index, setIndex] = useState<number>(0);// ^? const index: number
But, you'll also have noticed another behavior in React where in some cases, you don't need to pass the type argument in order for it to be inferred:
typescript
const [index, setIndex] = useState(0);// ^? const index: number// WHAT IS THIS SORCERY
To see what's going on here, let's look at our createSet
function again. Notice that it takes in no actual arguments- only type arguments:
typescript
export const createSet = <T>() => {return new Set<T>();}
What would happen if we change our function so that it accepts an initial value for the set?
We know that the initial value needs to be the same type as the Set, so let's type it as T
:
typescript
export const createSet = <T>(initial: T) => {return new Set<T>([initial]);}
Now, when we call it, we'll need to pass in an initial
, and that'll need to be the same type as the type argument we pass to createSet
.
typescript
const stringSet = createSet<string>("matt");// ^? const stringSet: Set<string>// Argument of type 'string' is not assignable to parameter of type 'number'.const numberSet = createSet<number>("pocock");
Here's the magical thing. TypeScript can infer the type of T
from initial
.
In other words, the type argument will be inferred from the runtime argument.
typescript
const stringSet = createSet("matt");// ^? const stringSet: Set<string>const numberSet = createSet(123);// ^? const numberSet: Set<number>
You can examine this by hovering over one of the createSet
calls. You'll see that <string>
is being inferred when we pass it a string
:
typescript
// hovering over createSetconst createSet: <string>(initial: string) => Set<string>
The same is true in useState
(see the useState<number>
syntax in the tooltip):
typescript
// hovering over useStateuseState<number>(initialState: number | (() => number)): [number, Dispatch<SetStateAction<number>>]
It's also true for this objKeys
function that includes some extra goodness as well:
T
to be an object so it can be passed to Object.keys
(which only accepts objects)Object.keys
to be Array<keyof T>
typescript
const objKeys = <T extends object>(obj: T) => {return Object.keys(obj) as Array<keyof T>;};
To sum it all up, what you think of as 'generics' are actually three different patterns:
DataShape<T>
createSet<string>()
If this felt like it went over your head, check out the free TypeScript Beginners tutorial.
On the other hand, if you want to go deeper into patterns, check out the Total TypeScript Pro Workshops!
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.