TypeScript with React: Setup
TypeScript is a strongly-typed superset of JavaScript that catches entire categories of bugs at compile time — before your code ever runs in a browser. Combined with React, it eliminates the most common runtime errors: wrong prop types, undefined property access, and mismatched function signatures. This page walks you through setting up a React + TypeScript project from scratch and understanding the configuration choices that matter most.
Creating a New Project
The fastest path to a React + TypeScript app is Vite with the official react-ts template. Run this single command:
npm create vite@latest my-app -- --template react-ts cd my-app npm install npm run dev
Vite scaffolds a project with TypeScript pre-configured, .tsx files for components, and type-checking baked into the build. The --template react-ts flag is key — without it you get a plain JavaScript project.
Key tsconfig.json Settings
The scaffolded tsconfig.json already has sane defaults, but it is worth understanding the options that matter for React development:
{
"compilerOptions": {
"target": "ESNext", // Compile to modern JS — Vite handles transpiling for older browsers
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"jsx": "react-jsx", // Use the new JSX transform (no need to import React in every file)
"strict": true, // Enables all strict checks — NEVER turn this off
"noUnusedLocals": true, // Error on unused variables
"noUnusedParameters": true, // Error on unused function parameters
"noFallthroughCasesInSwitch": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true // Vite handles output; tsc is type-check only
}
}The two most important settings are jsx: "react-jsx" and strict: true. The react-jsx transform means you no longer need import React from 'react' at the top of every file that contains JSX. strict: true bundles several compiler flags including strictNullChecks and noImplicitAny — leaving this off defeats much of the value TypeScript provides.
The .tsx Extension
TypeScript uses two file extensions for React projects:
Extension | Use for | Can contain JSX? |
|---|---|---|
.ts | Pure logic: utilities, hooks, services, types | No |
.tsx | Components and anything with JSX | Yes |
A simple rule: if the file contains angle brackets (<Component />), name it .tsx. Everything else can be .ts. Mixing these up causes TypeScript to report JSX as a syntax error.
Required Type Packages
React itself is written in JavaScript, so the TypeScript definitions live in separate @types packages. Vite installs them for you, but in a manual setup you add them yourself:
npm install --save-dev @types/react @types/react-dom
These packages provide the types for every React API: useState, useEffect, JSX.Element, React.ReactNode, event types, and more. Without them, TypeScript has no idea what React.FC or React.ChangeEvent mean.
How TypeScript Catches Prop Errors
Consider a UserCard component that requires a name and age prop. Without TypeScript you discover missing props at runtime (or not at all in testing). With TypeScript the error appears immediately in your editor:
interface UserCardProps {
name: string
age: number
}
function UserCard({ name, age }: UserCardProps) {
return (
<div>
<h2>{name}</h2>
<p>Age: {age}</p>
</div>
)
}
// ✓ Correct usage
<UserCard name="Alice" age={30} />
// ✗ TypeScript error: Property 'age' is missing
<UserCard name="Bob" />
// ✗ TypeScript error: Type 'string' is not assignable to type 'number'
<UserCard name="Carol" age="thirty" />The feedback is instant — no need to run the app, open a browser, or hit the specific code path. This is the core value proposition of TypeScript in React.
interface vs type for Props
TypeScript offers two ways to describe the shape of an object: interface and type. For React props, the convention is to use interface:
// ✓ Preferred for props — interface is declarative and extensible
interface ButtonProps {
label: string
onClick: () => void
disabled?: boolean
}
// Also valid — use type when you need union types or mapped types
type ButtonVariant = 'primary' | 'secondary' | 'ghost'
interface ButtonProps {
label: string
variant: ButtonVariant
}The practical difference is that interface supports declaration merging (adding properties from multiple places), while type is sealed after definition. For props that might be extended by consumers of a component library, interface is safer. For union types, computed types, or utility types, reach for type.
Avoid React.FC — Use Explicit Return Types
Older codebases annotate components with React.FC (Function Component). The React team now advises against it:
// ✗ Avoid — React.FC adds implicit children prop in older React versions
// and makes generic components awkward
const Button: React.FC<ButtonProps> = ({ label }) => <button>{label}</button>
// ✓ Preferred — plain function with typed props, implicit return type inferred
function Button({ label }: ButtonProps) {
return <button>{label}</button>
}
// ✓ Also fine — explicit return type if you want the guarantee
function Button({ label }: ButtonProps): JSX.Element {
return <button>{label}</button>
}JSX.Element is the narrower return type (a single element). React.ReactNode is broader — it includes null, undefined, strings, arrays, and fragments. Use JSX.Element as a return type when you know the component always returns markup, and React.ReactNode for the children prop type.
Running the Type Checker
Vite's dev server does not block on TypeScript errors — it transpiles quickly for fast feedback. To catch all type errors, run the TypeScript compiler separately:
npx tsc --noEmit # type-check only, no output files npx tsc --noEmit --watch # watch mode for continuous checking
Add npx tsc --noEmit to your CI pipeline so type errors block deployment. Many teams also integrate it as a pre-commit hook via Husky.