TypeScript + React in Practice #1: Getting Started and Setup

4 min read

If you’ve finished TypeScript Basics (#1–#7) and the React Basics course, it’s time the two met. This series organizes the patterns you meet in real work over six posts — the decisions you have to make as you move React code written in JavaScript into TypeScript.

Six posts in total.

  • #1 Getting started and setup ← this post
  • #2 Typing props and children
  • #3 Typing hooks (useState/useReducer/useRef)
  • #4 Typing events and forms
  • #5 Context and generic components
  • #6 Fetch and API response typing

This post covers why use TypeScript with React and goes all the way to creating a React + TS project with Vite and adding types to your first component.

Why TypeScript for React? #

You could fairly say React is just passing data between components via props. As components grow, these questions show up endlessly.

  • What props does this component take?
  • Are these props required or optional?
  • What arguments should onClick accept?
  • What does this hook return?

When writing JavaScript, you trace back to read the component body, log to the console, or check the screen to find a wrong value. Fine for a small app, but past 50 components the cost piles up fast.

With TypeScript, most of these questions become editor autocompletion and red squiggles.

JavaScript — wrong props only revealed at runtime
function UserCard({ name, age }) {
  return <div>{name} ({age})</div>;
}

// Parent component
<UserCard name="커티스" />              // age missing — still renders
<UserCard name="커티스" age="서른" />   // it's a string but renders
<UserCard nme="커티스" age={30} />      // typo — shown as undefined
TypeScript — caught the moment you write it
type UserCardProps = {
  name: string;
  age: number;
};

function UserCard({ name, age }: UserCardProps) {
  return <div>{name} ({age})</div>;
}

<UserCard name="커티스" />              // ✗ age is missing
<UserCard name="커티스" age="서른" />   // ✗ age must be number
<UserCard nme="커티스" age={30} />      // ✗ no prop named nme

All three common mistakes are caught immediately as red squiggles in the editor. The build also blocks them, so wrong code can’t reach users.

What TypeScript gives React #

Four headlines.

  1. Component contract — props shapes are explicit in code and verified at the call site.
  2. Autocompletionevent.target.value, useState’s return tuple, hook result objects all get inferred and autocompleted in the editor.
  3. Refactor safety — rename a prop or add/remove a field and every consumer lights up red at once.
  4. Code that needs no docs — the component signature alone tells you how to use it, reducing reliance on separate comments/docs.

Setup — start a React + TS project with Vite #

The lightest way to get a React + TypeScript environment is Vite. The command below creates a project in 5 seconds.

Create a new project
npm create vite@latest ts-react-playground -- --template react-ts
cd ts-react-playground
npm install
npm run dev

--template react-ts is the key. Vite sets up the following automatically.

  • tsconfig.json, tsconfig.app.json, tsconfig.node.json
  • @types/react and @types/react-dom for React 19
  • .tsx extension and strict mode
  • ESLint + a TypeScript rule set

Open http://localhost:5173 in a browser and you’ll see the default counter page.

A look at the project structure #

The key files in the generated project:

ts-react-playground/
├── src/
│   ├── App.tsx          # main component (note the .tsx extension)
│   ├── main.tsx         # entry point
│   ├── App.css
│   └── vite-env.d.ts    # Vite environment variable type declarations
├── index.html
├── tsconfig.json
├── tsconfig.app.json    # compile config for app code
├── tsconfig.node.json   # config for vite.config.ts
├── vite.config.ts
└── package.json

Two key points.

1) .tsx extension — A TypeScript file containing JSX is .tsx, not .ts. The compiler needs to know to recognize JSX syntax, so the extension distinguishes them.

2) strict mode — Vite’s react-ts template has "strict": true on by default. Strict mode is what makes TypeScript’s safety net meaningful. There may be many red squiggles at first, but never turn it off.

Open tsconfig.app.json and you’ll see lines like:

tsconfig.app.json (excerpt)
{
  "compilerOptions": {
    "target": "ES2022",
    "useDefineForClassFields": true,
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "moduleResolution": "bundler"
  },
  "include": ["src"]
}

Of these, two options stand out as most relevant to React work right away:

  • "jsx": "react-jsx" — Uses React 17+’s new JSX transform. You don’t have to write import React from 'react' at the top of every component file.
  • "strict": true — Turns on the core type-safety flags including strictNullChecks and noImplicitAny.

Add types to your first component #

Open src/App.tsx, delete the generated code, and replace it with:

src/App.tsx — Hello component
type HelloProps = {
  name: string;
};

function Hello({ name }: HelloProps) {
  return <h1>안녕하세요, {name}!</h1>;
}

function App() {
  return (
    <div>
      <Hello name="커티스" />
    </div>
  );
}

export default App;

Save and the browser shows “안녕하세요, 커티스님!”. Now intentionally call it wrong.

Wrong usage — see the red squiggles yourself
<Hello />                  // ✗ name missing
<Hello name={42} />        // ✗ should be string but is number
<Hello name="커티스" age={30} /> // ✗ no prop named age

All three get red squiggles immediately in the editor and npm run build is blocked. This is the most basic benefit we’ll get to enjoy through the rest of the series.

Component return types are usually inferred #

The basics course said to annotate function return types, but React function components usually skip the annotation. Inference is good enough, and an explicit annotation often limits your flexibility.

Over-annotation — not recommended
function Hello({ name }: HelloProps): React.ReactElement {
  return <h1>안녕하세요, {name}!</h1>;
}

If you pin it to React.ReactElement, when you later return null conditionally or return a fragment, the type won’t match and you’ll have to update it. Trusting inference, you can return all of these freely.

Trust inference — natural
function Hello({ name, hidden }: { name: string; hidden?: boolean }) {
  if (hidden) return null;            // OK
  return <h1>안녕하세요, {name}!</h1>; // OK
}

@types/react for React 19 infers every shape a component can return (element, string, null, fragment, etc.) automatically.

Older material often recommends React.FC<Props>, but the modern community has settled on the plain ({ ... }: Props) => ... pattern. FC has minor issues (forces children, etc.), so we don’t use it in this series either.

A common first impression — too many red squiggles #

Coming from JavaScript to TypeScript can feel like a fight against red squiggles. Two things to keep in mind until it feels natural:

1) Red squiggles are coworkers, not enemies. Each one shows you a “bug that would have hit at runtime if this code stayed as is” in advance. The first week or two can feel frustrating, but with time the sense flips to “huh, no red squiggle? Did I do something wrong?”

2) Silencing with any is a last resort. When stuck, the temptation to throw any at the problem is real, but unknown or a narrower type usually fits better. Once you use any, autocomplete and refactor safety vanish from that point on.

Wrap-up #

This post covered:

  • Why use TypeScript in React — component contracts, autocomplete, refactor safety
  • Setting up the environment with Vite + react-ts template
  • Meaning of the .tsx extension and strict mode
  • Adding props types to your first component
  • It’s natural to leave the return type to inference
  • Prefer ({ ... }: Props) => ... over React.FC

In the next post (#2 Typing props and children), we go deeper into props typing — optional props, union props, children patterns, and how types flow in compound components.

X