Command Palette

Search for a command to run...

Hectal
PHASE 12Intermediate ~28 min· topic 1 of 6

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 = 5 is a number.
tsc
The TypeScript compiler. tsc --noEmit only 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": true setting that turns on the important checks, such as null checks and no implicit any.
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.

terminalwhole filebash
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev

02Look 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/whole filebash
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.

Know who checks and who stripsdiagram
Rendering diagram…

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).

src/App.tsxwhole filetsx
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.

terminalwhole filebash
$ 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:9

06Fix 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. 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 a string. 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. 2

    Fix the server data

    In the second example, change fromServer so tags is an array: '{"name": "Asha", "tags": ["admin", "editor"]}'. Predict the console and the screen before running. (The trusting line now logs ADMIN, EDITOR; the checked version shows Asha: admin, editor.)

  3. 3

    Remove the escape hatch

    Delete the @ts-expect-error comment 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 underline nmae in red, and npm run build would stop.

Code & diagrams

The preview strips types, like Vite: a typo still runs New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Hello, Asha! You are 9.
Hello, ! Age unknown.
Types can't check data from outside: use unknown and a type guard New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

trusting: TypeError: trusted.tags.map is not a function
checked: the data does not match User
tsconfig.app.json from the Vite template (trimmed)json
{
  "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"]
}
Where type checking happensdiagram
Rendering diagram…

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.

terminal
$ npx tsc --noEmit, then open the page
── what you'll see ──
tsc: no errors.
Browser console:
Uncaught TypeError: Cannot read properties of undefined (reading 'toFixed')
An error occurred in the <Greeting> component.
 
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://react.dev/link/error-boundaries to learn more about error boundaries.

Break #2

Write JSX in a .ts file

Create src/hello.ts (not .tsx) containing export function Hello() { return <p>Hello</p>; }.

terminal
$ npx tsc --noEmit
── what you'll see ──
src/hello.ts:2:19 - error TS1161: Unterminated regular expression literal.
 
2 return <p>Hello</p>;
~~~
 
 
Found 1 error in src/hello.ts:2

Break #3

Trust data with as

Write const user = JSON.parse(text) as User; for data from a server and use user.tags.map(...).

terminal
$ Browser console
── what you'll see ──
TypeError: trusted.tags.map is not a function

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 JSX namespace**: function f(): JSX.Element now fails with Cannot find namespace 'JSX'. Use React.JSX.Element (or import type { JSX } from "react"), or better, let the return type be inferred. Other React 19 type changes you'll meet in this phase: useRef needs an argument, ref is an ordinary prop on function components, and ReactElement's props default to unknown instead of any.

  • ▸

    tsc -b (build mode) follows the references in tsconfig.json, so the app code (DOM types) and vite.config.ts (Node types) are checked with different settings, and unchanged projects are skipped using .tsbuildinfo files. In CI, tsc -b or tsc --noEmit -p tsconfig.app.json is 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 enum across files, or a re-export of a type without export type) can break at run time. That is why templates turn on isolatedModules-style settings (verbatimModuleSyntax in newer ones): they make the checker reject those patterns. Write import type { ReactNode } from "react" for type-only imports.

  • ▸

    @types/react evolves between React releases. In the 19.x types installed here, FormEvent and FormEventHandler are marked deprecated in favour of more precise ChangeEvent, InputEvent and SubmitEvent types (Topic 12.4). Deprecated types still compile; the editor shows them struck through.

Remember this

  1. 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 = 5 is a number); 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. 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 be null or undefined** (the cause of the famous Cannot read properties of undefined crash), reading .value from 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 passing undefined. Your editor also gets much better autocomplete, because it knows each component's props. What types don't catch: wrong logic (a + b when you meant a - b), and data that arrives while the app runs, such as a server response that doesn't match the type you wrote.

  3. 3

    **.tsx files:** a file that contains JSX must end in .tsx; plain TypeScript without JSX uses .ts. Write JSX in a .ts file and the parser reads <p> as the start of a comparison and fails with confusing errors. Inside .tsx, the old cast syntax <User>data is not allowed (it looks like a JSX tag), so you write data 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. 4

    Setup with Vite: npm create vite@latest my-app -- --template react-ts makes a React project with .tsx files, tsconfig files 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, and npm run build runs tsc -b before vite 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 exact tsc message.

  5. 5

    **@types/react:** the react package itself is plain JavaScript and ships no type information. The types live in separate packages, @types/react and @types/react-dom, written by the community project DefinitelyTyped. They describe every React API: what useState returns, 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. 6

    Strict mode ("strict": true in tsconfig.json, not React's <StrictMode>) switches on a family of checks. The two that matter most: **strictNullChecks**, which makes null and undefined separate 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, because name would silently be any, the type that switches checking off). New projects always use strict. Without it, TypeScript misses exactly the bugs that crash React apps most.

  7. 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 a number where a string was 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. IntrinsicAttributes in a message is the small set of props every component accepts (like key); you can ignore it. Fix the first error first: later errors are often caused by it.

Explain it without notes

01

What kinds of bugs does TypeScript catch in a React app, and what kinds does it miss?

02

Vite runs your .tsx files without checking types. Where does checking happen, and why is it set up that way?

03

What does "strict": true change, and why does it matter so much for React code?

04

How do you read Type '{ nmae: string; }' is not assignable to type 'IntrinsicAttributes & GreetingProps'.?

Practice

01

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.

02

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 tsc in the build and CI.

Done when you can

  • I can create a React + TypeScript project with Vite and explain its tsconfig files.

  • I know why Vite doesn't check types and where checking happens instead.

  • I know what @types/react is and why its version matters.

  • I can explain what strict turns on and why it matters for React.

  • I can read a tsc error: 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 unknown and a type guard.