TypeScript Basics for React
Recipe
Set up a TypeScript-powered React project with a well-configured tsconfig.json, understand how .tsx files work, and learn the core type annotations every React developer needs.
Working Example
// tsconfig.json (Next.js 15 / React 19 recommended)
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}// src/components/Greeting.tsx
type GreetingProps = {
name: string;
age?: number;
};
export function Greeting({ name, age }: GreetingProps) {
return (
<div>
<h1>Hello, {name}</h1>
{age !== undefined && <p>Age: {age}</p>}
</div>
);
}// Usage
<Greeting name="Alice" />
<Greeting name="Bob" age={30} />Deep Dive
How It Works
- TypeScript adds static type checking on top of JavaScript. The compiler catches type errors before your code runs in the browser.
.tsxfiles are TypeScript files that support JSX syntax. Thejsx: "preserve"setting tells TypeScript to leave JSX untouched for the bundler (Next.js, Vite) to handle.strict: trueenables a family of strict checks (strictNullChecks,strictFunctionTypes,noImplicitAny, etc.) that catch the most bugs.moduleResolution: "bundler"matches how modern bundlers resolve imports, supporting package.jsonexportsfields and extensionless imports.- Path aliases like
@/*let you writeimport { Button } from "@/components/Button"instead of fragile relative paths.
Variations
Vite + React setup:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"isolatedModules": true,
"skipLibCheck": true
},
"include": ["src"]
}Adding stricter options incrementally:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true
}
}TypeScript Notes
- Always install React types:
npm install -D @types/react @types/react-dom. With React 19, types ship withreactand@types/reactmay not be needed depending on your setup. - Use
typefor props and simple shapes; useinterfacewhen you need declaration merging orextends. - Prefer
unknownoverany. If you must useany, add a// eslint-disablecomment and a TODO to fix it later.
Gotchas
- Forgetting
"strict": truemeans TypeScript will not flagnullorundefinedaccess, defeating much of its value. - Setting
jsxto"react"instead of"preserve"or"react-jsx"forces you toimport React from "react"in every file. skipLibCheck: trueis recommended for build speed but can hide type errors in your own.d.tsfiles.- Using
anysilently disables type checking for everything that value touches downstream.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
strict: true from day one | Catches the most bugs early | Steeper learning curve for beginners |
Gradual adoption (strict: false) | Easier migration from JS | Misses critical null/undefined bugs |
JSDoc types (no .ts files) | Zero build step changes | Verbose, limited type expressiveness |
interface for all props | Declaration merging, familiar OOP style | Cannot express unions or mapped types |
type for all props | Unions, intersections, mapped types | No declaration merging |
FAQs
Why is strict: true recommended from day one in a React project?
- It enables
strictNullChecks,noImplicitAny,strictFunctionTypes, and other checks as a group. - Catches null/undefined access bugs that would otherwise surface only at runtime.
- Adding strict mode later to a large codebase requires fixing hundreds of errors at once.
What does jsx: "preserve" do and when should you use a different setting?
"preserve"leaves JSX untouched so the bundler (Next.js, Vite) handles the transformation.- Use
"react-jsx"for Vite + React projects that use the automatic JSX runtime. - Use
"react"only if you need the classicReact.createElementtransform (requires importing React in every file).
When should you use type vs interface for component props?
- Use
typewhen you need unions, intersections, or mapped types. - Use
interfacewhen you need declaration merging orextends. - For most React props,
typeis sufficient and more flexible.
What is the purpose of moduleResolution: "bundler" in tsconfig?
- It matches how modern bundlers like Next.js and Vite resolve imports.
- Supports package.json
exportsfields and extensionless imports. - Replaces older options like
"node"or"node16"for bundled projects.
Do you still need to install @types/react with React 19?
- React 19 ships its own types, so
@types/reactmay not be needed. - Check your setup -- some configurations still require the separate package.
- When in doubt, install it as a dev dependency and let TypeScript resolve conflicts.
What is the difference between .ts and .tsx files?
.tsxfiles support JSX syntax in addition to TypeScript..tsfiles are for pure TypeScript logic with no JSX.- Use
.tsxfor any file that returns or contains JSX elements.
Gotcha: What happens if you set skipLibCheck: true in your tsconfig?
- TypeScript skips type-checking all
.d.tsfiles, including your own custom declaration files. - This speeds up builds but can hide type errors in your declarations.
- Errors in third-party
.d.tsfiles are also silently ignored.
Why does using any defeat the purpose of TypeScript?
anysilently disables type checking for the value and everything it touches downstream.- Prefer
unknown-- it forces you to narrow or validate before accessing properties. - If you must use
any, add a comment explaining why and a TODO to fix it later.
What does the paths alias @/* do in tsconfig?
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}- Maps
@/components/Buttonto./src/components/Button. - Eliminates fragile relative paths like
../../../components/Button.
How do noUncheckedIndexedAccess and exactOptionalPropertyTypes add extra safety beyond strict: true?
noUncheckedIndexedAccessadds| undefinedto array and record index access, catching out-of-bounds bugs.exactOptionalPropertyTypesdistinguishes between a missing property and one set toundefined.- Neither is included in
strict: true-- they must be enabled separately.
What is the correct way to type an optional prop with a default value?
type GreetingProps = {
name: string;
age?: number; // optional
};
function Greeting({ name, age = 25 }: GreetingProps) {
return <p>{name} is {age} years old</p>;
}- Mark the prop as optional with
?in the type. - Provide the default in the destructuring pattern.
- TypeScript narrows the type inside the function body to exclude
undefined.