Skip to content
React rc typescript 4 min read

TypeScript with React

TypeScript adds a static type layer on top of JavaScript, and it pairs exceptionally well with React. Because a React component is just a function that receives props and returns JSX, the type system can describe exactly what those props look like, what state a hook holds, and what shape an event carries. The result is an editor that autocompletes prop names, catches typos before you save, and refactors components safely across a whole codebase. This page covers spinning up a typed React project with Vite, what .tsx files are, what types actually catch, and the tsconfig.json settings that matter.

Creating a typed Vite project

The fastest way to start is the official Vite scaffolder with the react-ts template. It wires up TypeScript, JSX, and the React plugin with sensible defaults so you can write typed components immediately.

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

Output:

VITE v6.0.0  ready in 312 ms

➜  Local:   http://localhost:5173/
➜  press h + enter to show help

The generated project uses ES modules, splits its TypeScript config across tsconfig.json / tsconfig.app.json / tsconfig.node.json, and includes @types/react and @types/react-dom for the type definitions of React’s APIs. Vite itself never type-checks during dev — it strips types with esbuild for speed — so you rely on your editor and tsc for actual type safety.

What .tsx files are

A .tsx file is a TypeScript file that may contain JSX. The x enables the JSX parser, exactly like .jsx does for plain JavaScript. Use .tsx for any module that returns markup and .ts for pure logic (utilities, hooks that return no JSX, type definitions).

// Greeting.tsx
type GreetingProps = {
  name: string;
  excited?: boolean;
};

function Greeting({ name, excited = false }: GreetingProps) {
  return <h1>Hello, {name}{excited ? "!" : "."}</h1>;
}

export default Greeting;

The GreetingProps type describes the component’s contract: name is required, excited is optional. Anyone rendering <Greeting /> now gets autocomplete for both props and an error if name is missing.

What static types catch in components

Types turn an entire class of runtime bugs into compile-time errors that surface in your editor. Consider the same component used incorrectly.

// Missing required prop
<Greeting excited />
// Error: Property 'name' is missing in type '{ excited: true; }'

// Wrong type for a prop
<Greeting name={42} />
// Error: Type 'number' is not assignable to type 'string'

// Typo in a prop name
<Greeting name="Ada" exited />
// Error: Property 'exited' does not exist on type 'GreetingProps'

Beyond props, the compiler checks state, function arguments, and return values. Here a typed useState ensures you only ever store the right shape, and the click handler is checked against the DOM event.

import { useState } from "react";

type Status = "idle" | "loading" | "done";

function Loader() {
  const [status, setStatus] = useState<Status>("idle");

  function start() {
    setStatus("loading");
    // setStatus("ready"); // Error: not assignable to type 'Status'
  }

  return (
    <button onClick={start} disabled={status === "loading"}>
      {status === "loading" ? "Working..." : "Start"}
    </button>
  );
}

The union type Status means an invalid string is rejected immediately, and narrowing on status === "loading" is fully understood by the compiler.

Tip: Avoid typing component props as the catch-all React.FC. It implicitly adds children, complicates generic components, and offers no real benefit over typing the props parameter directly with a type or interface.

tsconfig basics

The tsconfig.json controls how strictly TypeScript checks your code and how it interprets JSX. The Vite template ships with strong defaults; these are the options worth understanding.

OptionRecommended valueWhat it does
stricttrueEnables all strict checks (null safety, implicit any, etc.) — the single most valuable setting
jsx"react-jsx"Uses the modern automatic JSX runtime, so you don’t import React in every file
target"ES2020" or laterThe JavaScript version tsc emits and type-checks against
moduleResolution"bundler"Matches how Vite/esbuild resolve imports, including extensionless paths
noUnusedLocalstrueFlags dead variables to keep components clean
skipLibChecktrueSkips type-checking .d.ts files in dependencies for faster builds

A typical app config looks like this:

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noEmit": true
  },
  "include": ["src"]
}

Because Vite handles the actual bundling, noEmit is true — TypeScript only type-checks, it never produces output files. Add a check to your package.json scripts so CI catches type errors that the dev server silently ignores.

{
  "scripts": {
    "dev": "vite",
    "build": "tsc --noEmit && vite build",
    "typecheck": "tsc --noEmit"
  }
}

Running npm run typecheck validates the whole project at once, which is the safety net Vite’s fast dev transform leaves out.

Best Practices

  • Enable strict mode from day one; turning it on later in a large codebase is painful.
  • Use .tsx for files containing JSX and .ts for pure logic, hooks without markup, and shared types.
  • Type the props parameter directly rather than reaching for React.FC.
  • Prefer union string literals (like "idle" | "loading") over loose string for finite sets of values.
  • Run tsc --noEmit in CI and your build script, since Vite’s dev server does not type-check.
  • Let inference do the work for useState when the initial value is unambiguous; supply an explicit type argument only when it cannot be inferred.
Last updated June 14, 2026
Was this helpful?