TypeScript + React Setup
Vite + TypeScript setup, the key tsconfig options, and your first .tsx file. The foundation we build Part 3 of this book on.
Chapter 15 closed out Part 2. Across Parts 1 and 2 we put components, props, state, events, forms, useEffect, lifting state up, Context, custom hooks, performance, and routing into our hands. This chapter opens Part 3. We are going to put every piece of code we have seen so far back on top of TypeScript.
In truth every example in this book could have used TypeScript from the start, and the model this book stands behind is “TypeScript first”. Parts 1 and 2 stayed on JavaScript so we could focus on React’s core concepts first. Now that we layer TypeScript’s safety net on top, every piece of code from Part 3 onward (Next.js, RSC, Server Actions, fullstack) flows naturally on top of TypeScript.
Why use TypeScript with React #
It is not an exaggeration to say React is, at the end of the day, just passing data between components through props. As your components grow, the same questions keep coming back.
- What props does this component take?
- Is this prop required or optional?
- What argument should
onClickreceive? - What does this hook return?
In JavaScript you trace the code back, read the component body directly, log to the console, or check the screen to see whether something was passed wrong. That is fine in a small app, but once you cross fifty components, the cost compounds quickly.
When TypeScript enters the picture, most of those questions turn into editor autocomplete and red underlines.
function UserCard({ name, age }) {
return <div>{name} ({age})</div>;
}
// parent component
<UserCard name="Curtis" /> // age missing, renders anyway
<UserCard name="Curtis" age="thirty" /> // a string, renders anyway
<UserCard nme="Curtis" age={30} /> // typo — shows undefinedtype UserCardProps = {
name: string;
age: number;
};
function UserCard({ name, age }: UserCardProps) {
return <div>{name} ({age})</div>;
}
<UserCard name="Curtis" /> // ✗ age is missing
<UserCard name="Curtis" age="thirty" /> // ✗ age must be a number
<UserCard nme="Curtis" age={30} /> // ✗ there is no prop named nme
All three of these common mistakes are caught as red underlines in the editor immediately. The build is also blocked, so the wrong code never reaches the user.
What TypeScript gives React #
Four points sum it up.
- A component contract — the shape of props is written in code, and the caller side is checked right there.
- Autocomplete —
event.target.value, the tuple returned byuseState, and the result objects from hooks are all inferred so the editor can autocomplete them. - Safe refactors — when you rename a prop or add/remove a field, every place that uses it lights up with a red underline at once.
- Code that needs no docs — the component signature alone tells you how to use it, so dependence on separate comments or docs goes down.
Setup — starting React + TS with Vite #
The lightest way to get a React + TypeScript environment going is Vite. On top of the environment we set up in Chapter 2, you only have to add the --template react-ts option.
pnpm create vite@latest ts-react-playground --template react-ts
cd ts-react-playground
pnpm install
pnpm dev--template react-ts is the key. Vite sets up the following for you:
tsconfig.json,tsconfig.app.json,tsconfig.node.json@types/react,@types/react-domfor React 19- the
.tsxextension and strict mode - a full ESLint + TypeScript rule set
Open http://localhost:5173 in the browser and the default counter page shows up.
If you already have a project from the Part 1 and 2 examples, you can adopt TypeScript on top of it gradually. Starting fresh is cleaner, though, so from this chapter on we recommend working in the new project above.
Looking around the project #
The key files in the generated project are:
├── src/
│ ├── App.tsx # main component (note the .tsx extension)
│ ├── main.tsx # entry point
│ ├── App.css
│ └── vite-env.d.ts # type declarations for Vite environment variables
├── index.html
├── tsconfig.json
├── tsconfig.app.json # compile settings for app code
├── tsconfig.node.json # settings for vite.config.ts
├── vite.config.ts
└── package.jsonTwo things matter.
1) The .tsx extension — a TypeScript file that contains JSX is .tsx, not .ts. The compiler needs to know to parse JSX syntax, and the extension is what tells it.
2) Strict mode — Vite’s react-ts template ships with "strict": true on by default. This needs to be on for TypeScript’s safety net to mean anything. The red underlines may feel overwhelming at first, but do not turn this off.
If you open tsconfig.app.json, you will see lines like the following:
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "bundler"
},
"include": ["src"]
}Among these, the two options that matter first in React work are:
"jsx": "react-jsx"— uses the new JSX transform from React 17+. You do not have to writeimport React from 'react'at the top of every component file."strict": true— turns on every core type-safety flag likestrictNullChecksandnoImplicitAny.
Every piece of code in this book assumes strict mode is on.
Typing your first component #
Open src/App.tsx, erase everything that was generated, and replace it with this:
type HelloProps = {
name: string;
};
function Hello({ name }: HelloProps) {
return <h1>Hello, {name}!</h1>;
}
function App() {
return (
<div>
<Hello name="Curtis" />
</div>
);
}
export default App;Save it and the browser shows “Hello, Curtis!”. Now try calling it wrong on purpose.
<Hello /> // ✗ name is missing
<Hello name={42} /> // ✗ should be a string but is a number
<Hello name="Curtis" age={30} /> // ✗ no prop named age
All three get red underlines in the editor immediately, and pnpm build is blocked too. This is the most basic benefit you will enjoy from Part 3 of this book.
Leave the return type of a component to inference #
You are often taught to write explicit return types on functions, but with React function components, you usually do not. Inference does the job well, and explicit return types often make the function less expressive.
function Hello({ name }: HelloProps): React.ReactElement {
return <h1>Hello, {name}!</h1>;
}Pin it to React.ReactElement and later, when you want to return null conditionally or a fragment, the type stops fitting and you have to come back to fix it. Leave it to inference and you can freely return all of:
function Hello({ name, hidden }: { name: string; hidden?: boolean }) {
if (hidden) return null; // OK
return <h1>Hello, {name}!</h1>; // OK
}@types/react for React 19 infers every shape a component can return (elements, strings, null, fragments, and so on) on its own.
React.FC<Props>. The community has now settled on just using the ({ ... }: Props) => ... pattern. FC has small annoyances such as forcing children to be accepted, so this book does not use it either. Appendix A (Migrating Old React) covers the procedure to move from FC to function + props destructuring.A common first impression — there are too many red underlines #
Coming to TypeScript from JavaScript, the first feeling is often that you are fighting red underlines. Two things are worth keeping in mind until it becomes natural.
1) Red underlines are not enemies but co-workers. Each one is showing you “a bug this code as written would have hit at runtime” up front. The first week or two can feel awkward, but as time goes on the feeling flips: “Hm, no red line? Did I really not make a mistake?”
2) Silencing things with any is the last resort. When you get stuck, the temptation is to wave it away with any, but in most of those cases unknown or a narrower type fits better. Once you use any, every bit of autocomplete and refactor safety in that area disappears. This book does not use any in example code.
Try it yourself #
Let’s rewrite a few of the components from Parts 1 and 2 in TypeScript.
src/Counter.tsx:
import { useState } from 'react';
type CounterProps = {
initial?: number;
};
function Counter({ initial = 0 }: CounterProps) {
const [count, setCount] = useState(initial);
return (
<div style={{ padding: '16px', border: '1px solid #ccc', borderRadius: '8px' }}>
<h2>Count: {count}</h2>
<button onClick={() => setCount(prev => prev + 1)}>+1</button>
<button onClick={() => setCount(prev => prev - 1)}>-1</button>
<button onClick={() => setCount(initial)}>Reset</button>
</div>
);
}
export default Counter;src/App.tsx:
import Counter from './Counter';
function App() {
return (
<>
<h1>TypeScript Counter</h1>
<Counter />
<Counter initial={10} />
</>
);
}
export default App;Save and confirm it works in the browser. Then pass a wrong type like <Counter initial="10" />. A red underline shows up in the editor and pnpm build is blocked too.
Exercises #
- Make
Counter’sinitialprop required (initial: number) and try calling it from the parent as<Counter />with the prop missing. Confirm how the red underline looks. - In the editor, check the inferred type of
useState(0)(hover over the variable in VS Code). You will seecount: numberandsetCount: Dispatch<SetStateAction<number>>. Then change it touseState('hello')and observe how the inference follows along tocount: string. - Put a deliberate
anyusage like(initial as any).toFixed(2)insideCounter. Watch how autocomplete and refactor safety disappear from that point on. Then switch tounknownand observe how the compiler forces narrowing (typeof initial === 'number').
In one line: Part 3 of this book is TypeScript first. Start with the Vite + react-ts template and keep the
.tsxextension and strict mode as defaults. Define props on your first component withtypeand leave return types to inference. Use the({ ... }: Props) => ...pattern instead ofReact.FC.anyis the last resort, and red underlines are co-workers, not enemies.
Next chapter #
In the next Chapter 17 Typing props and children we go deeper into props typing. We cover optional props, union props, receiving HTML attributes with ComponentProps<'button'>, the difference between ReactNode and ReactElement, and when to reach for PropsWithChildren, all in one chapter.