Topic 12.1
Why TypeScript with React
In one line
TypeScript adds types to JavaScript so a checker can catch wrong, missing and misspelled props, and values that might be null, while you type; React projects use .tsx files, the @types/react package and strict mode, and Vite strips the types without checking them.
Think of it like this
A shape-sorter toy. The square block only fits the square hole, so a toddler finds out at once that the triangle doesn't go there, not an hour later when the box won't close. TypeScript gives every hole in your code a shape ("this prop must be text", "this might be empty"). If you push in the wrong shape, your editor underlines it in red before the app ever runs.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- TypeScript
- JavaScript plus types. A checker reads the types, reports mistakes, and then the types are removed.
- Type
- A description of allowed values:
string,number,boolean, or an object shape like{ name: string }. - Type annotation
- A type you write yourself after a colon:
name: string. - Type inference
- TypeScript working out a type from the value, so you don't have to write it:
const n = 5is anumber. - tsc
- The TypeScript compiler.
tsc --noEmitonly checks types and prints errors; it writes no files. - .tsx
- The file extension for TypeScript files that contain JSX.
- @types/react
- The package that describes React's APIs to TypeScript. React itself ships no types.
- Strict mode (TypeScript)
- The
"strict": truesetting that turns on the important checks, such as null checks and no implicitany. - any
- A type that switches checking off for a value. Anything goes, so mistakes go unnoticed.
- Type error
- A message from the checker that a value's type doesn't fit where it is used.
Step by step
01Create a React + TypeScript project
The Vite template react-ts sets up everything: React, TypeScript, the type packages and the config files. You need Node.js installed.
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev02Look at what it made
The project looks roughly like this (the exact files change a little between template versions). Notice .tsx for files with JSX, and three tsconfig files: one for your app code, one for vite.config.ts (which runs in Node), and one that ties them together.
package.json lists typescript, @types/react and @types/react-dom as dev dependencies, and its build script is tsc -b && vite build: check first, then bundle.
my-app/
index.html # the one HTML page, loads src/main.tsx
package.json # scripts: dev, build (tsc -b && vite build), lint, preview
vite.config.ts # Vite settings, with the React plugin
tsconfig.json # points to the two files below
tsconfig.app.json # TypeScript settings for src/ (strict: true)
tsconfig.node.json # TypeScript settings for vite.config.ts
eslint.config.js
public/
src/
main.tsx # createRoot(document.getElementById('root')!).render(...)
App.tsx
App.css
index.css
assets/03Know who checks and who strips
Three tools touch your .tsx files. Only two of them check types. If you only ever run npm run dev and ignore the editor, type errors never stop you; that's why npm run build (and CI) run tsc.
04Describe a component's props
Write a type for the props and annotate the destructured parameter with it. age? with a question mark means the prop is optional: its type becomes number | undefined (a number, or nothing).
type GreetingProps = {
name: string;
age?: number;
};
function Greeting({ name, age }: GreetingProps) {
return (
<p>
Hello, {name.toUpperCase()}! You are {age.toFixed(0)}.
</p>
);
}
export default function App() {
return (
<>
<Greeting name={42} />
<Greeting nmae="Asha" />
</>
);
}05Let the checker find three bugs
Run npx tsc --noEmit (check only, write nothing). Every one of these would have been a silent bug or a crash in plain JavaScript: age can be missing, 42 is not text, and nmae is a typo, so name would be undefined.
$ npx tsc --noEmit
src/App.tsx:9:45 - error TS18048: 'age' is possibly 'undefined'.
9 Hello, {name.toUpperCase()}! You are {age.toFixed(0)}.
~~~
src/App.tsx:17:17 - error TS2322: Type 'number' is not assignable to type 'string'.
17 <Greeting name={42} />
~~~~
src/App.tsx:2:3
2 name: string;
~~~~
The expected type comes from property 'name' which is declared here on type 'IntrinsicAttributes & GreetingProps'
src/App.tsx:18:17 - error TS2322: Type '{ nmae: string; }' is not assignable to type 'IntrinsicAttributes & GreetingProps'.
Property 'nmae' does not exist on type 'IntrinsicAttributes & GreetingProps'.
18 <Greeting nmae="Asha" />
~~~~
Found 3 errors in the same file, starting at: src/App.tsx:906Fix them one by one
Handle the missing age (age === undefined ? ... : ...), pass a string, and spell the prop correctly. When tsc prints nothing, every caller matches every component.
For the second error, notice the extra block under it: TypeScript also shows where the expected type came from (line 2, name: string). That is often the fastest way to understand an error about a prop.
07Remember the limit: types vanish at run time
Types describe what you promise; they don't test real data. JSON.parse returns any, so a function can claim to return a User while the server sent something else. At the edges of your app (server responses, localStorage, URL parameters), give the value the type unknown and check it before use. The second live example below shows both ways.
Try it yourself
- 1
Predict the empty greeting
In the first example, add
<Greeting name="" />. Predict what the page shows (Hello, ! Age unknown.). TypeScript is happy: an empty string is still astring. Types check the kind of value, not whether it is sensible.Then add
<Greeting name="Mei" age={30} />and check the new line appears. - 2
Fix the server data
In the second example, change
fromServersotagsis an array:'{"name": "Asha", "tags": ["admin", "editor"]}'. Predict the console and the screen before running. (The trusting line now logsADMIN, EDITOR; the checked version showsAsha: admin, editor.) - 3
Remove the escape hatch
Delete the
@ts-expect-errorcomment line in the first example. The preview still runs exactly the same, because it doesn't check types. In a real Vite project your editor would underlinenmaein red, andnpm run buildwould stop.
Code & diagrams
The second greeting has no name, so it prints `Hello, !`. The code runs because types are erased; only the checker (your editor or `tsc`) would have stopped you. `// @ts-expect-error` tells `tsc` "an error is expected on the next line", so this file still type-checks; use it only in teaching code and tests. The `!` after `getElementById("root")` tells TypeScript the element exists (without it: `Argument of type 'HTMLElement | null' is not assignable to parameter of type 'Container'.`).
Preview
Console
Nothing logged yet.Expected console output
Hello, Asha! You are 9.
Hello, ! Age unknown.Both versions type-check with zero errors. The trusting one crashes at run time (caught here so you can read it), because the type was only a promise. The checked one gives the value the type `unknown`, which TypeScript won't let you use until a check proves its shape; `value is User` is a **type guard**: when `isUser` returns `true`, TypeScript treats the value as a `User`. Libraries such as Zod write these checks for you.
Preview
Console
Nothing logged yet.Expected console output
trusting: TypeError: trusted.tags.map is not a function
checked: the data does not match User{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Turn off strict mode
Set "strict": false in tsconfig.app.json, write function Greeting({ name, age }) with no types, and render <Greeting name="Asha" /> with {age.toFixed(0)} in the JSX.
Break #2
Write JSX in a .ts file
Create src/hello.ts (not .tsx) containing export function Hello() { return <p>Hello</p>; }.
Break #3
Trust data with as
Write const user = JSON.parse(text) as User; for data from a server and use user.tags.map(...).
Myth vs fact
Myth
TypeScript makes my app faster or safer at run time.
Fact
Types are erased before the code runs; the browser runs the same JavaScript either way. TypeScript makes you faster and safer while writing: fewer bugs reach the browser, and refactors are less scary.
Myth
If it type-checks, it works.
Fact
Types check the shape of values, not your logic, and not data that arrives while the app runs. You still need tests (Phase 17) and validation at the edges of the app.
Myth
Vite shows type errors in the browser.
Fact
Vite only strips types; the dev server happily runs code with type errors. Errors appear in your editor and when npm run build runs tsc -b. Run tsc in CI so nobody merges red code.
Myth
React ships its own TypeScript types.
Fact
React's packages are plain JavaScript. The types come from @types/react and @types/react-dom (DefinitelyTyped). Keep their major version in step with React.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
React 19's types removed the **global
JSXnamespace**:function f(): JSX.Elementnow fails withCannot find namespace 'JSX'.UseReact.JSX.Element(orimport type { JSX } from "react"), or better, let the return type be inferred. Other React 19 type changes you'll meet in this phase:useRefneeds an argument,refis an ordinary prop on function components, andReactElement's props default tounknowninstead ofany. - ▸
tsc -b(build mode) follows thereferencesintsconfig.json, so the app code (DOM types) andvite.config.ts(Node types) are checked with different settings, and unchanged projects are skipped using.tsbuildinfofiles. In CI,tsc -bortsc --noEmit -p tsconfig.app.jsonis the gate. - ▸
Because Vite and this preview compile one file at a time without type information, a few TypeScript features that need the whole program to compile (such as
const enumacross files, or a re-export of a type withoutexport type) can break at run time. That is why templates turn onisolatedModules-style settings (verbatimModuleSyntaxin newer ones): they make the checker reject those patterns. Writeimport type { ReactNode } from "react"for type-only imports. - ▸
@types/reactevolves between React releases. In the 19.x types installed here,FormEventandFormEventHandlerare marked deprecated in favour of more preciseChangeEvent,InputEventandSubmitEventtypes (Topic 12.4). Deprecated types still compile; the editor shows them struck through.
Remember this
- 1
TypeScript is JavaScript with types added. A type describes what kind of value is allowed:
string(text),number,boolean(true or false), an object shape like{ name: string; age?: number }, and so on. You write a type annotation after a colon:function Greeting({ name }: { name: string }). Often you don't need to write one, because TypeScript works the type out from the value (const n = 5is anumber); that is called type inference. A program called the type checker (tsc, the TypeScript compiler, or the same engine running inside your editor) reads the types and reports mistakes. Then the types are erased: the browser only ever runs plain JavaScript. - 2
What types catch in React code: passing a number where a component expects text (
<Greeting name={42} />), forgetting a required prop, misspelling a prop (nmae), using a value that **might benullorundefined** (the cause of the famousCannot read properties of undefinedcrash), reading.valuefrom an event that has none, and typos in reducer action names. The biggest daily win is refactoring: rename a prop and every place that uses the old name turns red, instead of silently passingundefined. Your editor also gets much better autocomplete, because it knows each component's props. What types don't catch: wrong logic (a + bwhen you meanta - b), and data that arrives while the app runs, such as a server response that doesn't match the type you wrote. - 3
**
.tsxfiles:** a file that contains JSX must end in.tsx; plain TypeScript without JSX uses.ts. Write JSX in a.tsfile and the parser reads<p>as the start of a comparison and fails with confusing errors. Inside.tsx, the old cast syntax<User>datais not allowed (it looks like a JSX tag), so you writedata as User, and a generic arrow function needs a trailing comma,<T,>(x: T) => x, so it isn't read as a tag (Topic 12.6). - 4
Setup with Vite:
npm create vite@latest my-app -- --template react-tsmakes a React project with.tsxfiles,tsconfigfiles and the type packages already installed. Vite does not check types. Its dev server strips them (a fast tool, esbuild, removes the annotations) so the page reloads in milliseconds. Checking happens in two other places: your editor shows red underlines as you type, andnpm run buildrunstsc -bbeforevite build, so a type error stops the build. This live preview behaves like Vite: it strips types and runs the code, so below, every type error is shown in a comment or a separate block with the exacttscmessage. - 5
**
@types/react:** thereactpackage itself is plain JavaScript and ships no type information. The types live in separate packages,@types/reactand@types/react-dom, written by the community project DefinitelyTyped. They describe every React API: whatuseStatereturns, which props a<button>accepts, what an event object contains. Keep their major version the same as React's (19 with React 19): a mismatch gives confusing errors about features that do or don't exist. - 6
Strict mode (
"strict": trueintsconfig.json, not React's<StrictMode>) switches on a family of checks. The two that matter most: **strictNullChecks**, which makesnullandundefinedseparate types, so the checker says'age' is possibly 'undefined'until you handle the empty case, and **noImplicitAny**, which refuses parameters with no type (function Greeting({ name })is an error, becausenamewould silently beany, the type that switches checking off). New projects always use strict. Without it, TypeScript misses exactly the bugs that crash React apps most. - 7
Reading a type error:
src/App.tsx:17:17 - error TS2322: Type 'number' is not assignable to type 'string'.means: in this file, line 17, column 17, you gave anumberwhere astringwas expected. "X is not assignable to Y" always means "you gave X, the slot needs Y". Long errors are nested: the last, most indented line is usually the real reason, so read from the bottom.IntrinsicAttributesin a message is the small set of props every component accepts (likekey); you can ignore it. Fix the first error first: later errors are often caused by it.
Explain it without notes
What kinds of bugs does TypeScript catch in a React app, and what kinds does it miss?
Vite runs your .tsx files without checking types. Where does checking happen, and why is it set up that way?
What does "strict": true change, and why does it matter so much for React code?
How do you read Type '{ nmae: string; }' is not assignable to type 'IntrinsicAttributes & GreetingProps'.?
Practice
Write a typed PriceTag component with a required amount: number and an optional currency?: string that defaults to "₹". Render it twice, once without a currency.
Write a type guard isProduct(value: unknown): value is Product for type Product = { id: number; title: string }, use it on JSON.parse('{"id": "7", "title": "Pen"}'), and log whether the data is valid.
Trade-offs
- ↔
TypeScript adds a build step, config files and some typing effort, and its error messages take practice to read. In return, refactors become safe and a whole class of crashes disappears. For anything bigger than a demo, teams find it pays for itself quickly.
- ↔
Stripping types (Vite, esbuild, this preview) keeps development fast but means type errors don't stop the dev server. You must keep an eye on the editor and run
tscin the build and CI.
Done when you can
I can create a React + TypeScript project with Vite and explain its
tsconfigfiles.I know why Vite doesn't check types and where checking happens instead.
I know what
@types/reactis and why its version matters.I can explain what
strictturns on and why it matters for React.I can read a
tscerror: file, line, code, and the "not assignable" sentence from the bottom up.I know types are erased at run time and validate outside data with
unknownand a type guard.