| |

TypeScript 88 🔷 TypeScript with React

React and TypeScript are two tools that solve different problems. React manages the UI. TypeScript manages the shapes of data. Together, they form a development environment where the compiler catches most of the mistakes that would otherwise surface at runtime — wrong prop names, missing state properties, event handlers with the wrong signature, and API responses that do not match what the component expects.

The React documentation states it plainly: “TypeScript is a popular way to add type definitions to JavaScript codebases” . The @types/react and @types/react-dom packages provide the type definitions for React itself, and TypeScript understands JSX natively once those are installed . This chapter covers the patterns that matter: typing component props, using hooks with TypeScript, handling events, and the strictness settings that pay off most in React codebases.

Key point: Every file that contains JSX must use the .tsx extension. Files that contain only TypeScript — no JSX — use .ts. The tsconfig.json must set "jsx": "preserve" (or "react-jsx" for the automatic runtime), and the lib array must include "dom" . Without these settings, TypeScript will not recognize JSX syntax or the DOM types that event handlers rely on.


Why TypeScript and React work well together

React’s component model is built on props and state. Both are objects with specific shapes. TypeScript’s structural type system is designed to describe the shapes of objects.

The prop contract problem. A React component declares what props it accepts through its function signature. Without types, the contract is implicit. A caller can pass titl instead of title, and the component silently ignores it. With TypeScript, the interface is explicit, the compiler checks every call site, and the editor autocompletes the prop names .

The state shape problem. useState infers the type from the initial value. useState(false) gives boolean. useState("idle") gives string. When the state can be one of several specific values, the inferred type is too wide. TypeScript lets you narrow it with a type argument: useState<Status>("idle"), where Status is a union of the allowed strings .

The event problem. React events are not plain DOM events. They are synthetic events with a specific shape. React.ChangeEvent<HTMLInputElement> is the type for the change event on an input. React.FormEvent<HTMLFormElement> is the type for a form submission. Without these types, event.target.value produces a type error because target is not guaranteed to be an input .

The ecosystem problem. Redux Toolkit, TanStack Query, and Zod all have TypeScript integrations. Redux Toolkit lets you infer RootState and AppDispatch from the store. TanStack Query infers the data type from the query function. Zod lets you derive TypeScript types from validation schemas using z.infer . These integrations mean the type system extends from the component through the data layer to the API.

The trade-off. TypeScript adds ceremony. Every prop needs a type. Every event handler needs a signature. For a small prototype, the overhead is noticeable. For a production application with multiple developers, the overhead is repaid in fewer runtime errors and better editor support.


a. Typing Component Props

The simplest way to type a component is to inline the type in the function parameter :

function MyButton({ title }: { title: string }) {
  return <button>{title}</button>;
}

For components with more than two or three props, an interface or type is more readable :

interface MyButtonProps {
  title: string;
  disabled?: boolean;
}

function MyButton({ title, disabled = false }: MyButtonProps) {
  return <button disabled={disabled}>{title}</button>;
}

The disabled prop is optional because of the ?. The default value false is applied when the prop is omitted. TypeScript checks that every required prop is provided at the call site and that no unknown props are passed .

For components that accept children, the type is React.ReactNode :

type CardProps = {
  title: string;
  children: React.ReactNode;
};

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div>{children}</div>
    </div>
  );
}

React.ReactNode covers everything React can render: strings, numbers, JSX elements, arrays of these, null, and undefined . It is the correct type for the children prop in almost every case.

The React.FC type — React.FunctionComponent — was once the standard way to type components. It is now discouraged. The consensus is that a plain function with typed props is clearer and avoids the historical children implicit prop issue . The React documentation does not use React.FC in its TypeScript examples .


b. Hooks with TypeScript

The hooks that ship with @types/react infer types from the values you pass. The type arguments are only needed when the inferred type is too wide or when the state starts as null.

useState infers from the initial value. useState("idle") infers string, which is too broad if the state should be a union . The fix is an explicit type argument:

type Status = "idle" | "loading" | "success" | "error";

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

When the state starts as null and is set later, the type must be a union with null :

const [user, setUser] = useState<User | null>(null);

useReducer infers the state type from the initial state. The recommended pattern is to type the initial state, not the hook call :

interface State {
  count: number;
}

type CounterAction =
  | { type: "reset" }
  | { type: "setCount"; value: State["count"] };

const initialState: State = { count: 0 };

function stateReducer(state: State, action: CounterAction): State {
  switch (action.type) {
    case "reset":
      return initialState;
    case "setCount":
      return { ...state, count: action.value };
  }
}

The discriminated union CounterAction ensures that the value property is only accessible when action.type is "setCount". TypeScript narrows the action type inside each case branch .

useContext infers the value type from the createContext call. When the context has no sensible default, the type should include null, and the consuming hook should throw if the value is missing :

type Theme = "light" | "dark";

const ThemeContext = createContext<Theme | null>(null);

const useGetTheme = () => {
  const theme = useContext(ThemeContext);
  if (!theme) throw new Error("useGetTheme must be used within a Provider");
  return theme;
};

The runtime check removes null from the type in the hook’s return, so consumers do not need to handle it .


c. Event Handlers and Strictness

React event handlers are typed with the React.ChangeEvent, React.FormEvent, React.MouseEvent, and similar types. The generic parameter specifies the element the event is attached to :

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setEmail(e.target.value);
};

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  const formData = new FormData(e.currentTarget);
};

The React.ChangeEvent<HTMLInputElement> type guarantees that e.target is an HTMLInputElement, so e.target.value is a string . Without the type, e.target is EventTarget, and value does not exist on it.

The strictness settings in tsconfig.json determine how many of these errors TypeScript catches. The four options with the highest return for React projects are :

strict: true enables all strict checks. It is the recommended starting point for new projects.

noImplicitAny: true prevents TypeScript from falling back to any when it cannot infer a type. This catches untyped props, untyped event handlers, and untyped useState calls .

strictNullChecks: true makes TypeScript distinguish between T and T | null | undefined. This is the single most valuable option for React. Optional props, API responses that might be null, and async state all become safer .

noImplicitReturns: true ensures every code path in a function returns a value. React components must return JSX or null. Without this option, a component that returns undefined in some branch compiles but crashes at runtime .

strictFunctionTypes: true checks function parameter types more rigorously. It catches event handlers and callbacks that accept the wrong parameter type .


Complete Example Session

This session builds a typed form component, a typed reducer, a typed context, and a component that uses all three.

// ============================================
// PART 1: THE TYPED PROPS
// ============================================

interface User {
  name: string;
  email: string;
}

interface ProfileCardProps {
  user: User;
  onEdit: (user: User) => void;
  children?: React.ReactNode;
}

function ProfileCard({ user, onEdit, children }: ProfileCardProps) {
  return (
    <div className="card">
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <button onClick={() => onEdit(user)}>Edit</button>
      {children}
    </div>
  );
}

// ============================================
// PART 2: THE TYPED USESTATE
// ============================================

type Status = "idle" | "loading" | "success" | "error";

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

// ============================================
// PART 3: THE TYPED USEREDUCER
// ============================================

interface FormState {
  text: string;
  email: string;
}

type FormAction =
  | { type: "setText"; value: string }
  | { type: "setEmail"; value: string }
  | { type: "reset" };

const initialFormState: FormState = { text: "", email: "" };

function formReducer(state: FormState, action: FormAction): FormState {
  switch (action.type) {
    case "setText":
      return { ...state, text: action.value };
    case "setEmail":
      return { ...state, email: action.value };
    case "reset":
      return initialFormState;
  }
}

// ============================================
// PART 4: THE TYPED USECONTEXT
// ============================================

type Theme = "light" | "dark";

const ThemeContext = createContext<Theme | null>(null);

function useTheme() {
  const theme = useContext(ThemeContext);
  if (!theme) throw new Error("useTheme must be used within ThemeProvider");
  return theme;
}

// ============================================
// PART 5: THE TYPED EVENT HANDLERS
// ============================================

function TypedForm() {
  const [state, dispatch] = useReducer(formReducer, initialFormState);

  const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    dispatch({ type: "setText", value: e.target.value });
  };

  const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    dispatch({ type: "setEmail", value: e.target.value });
  };

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log(state);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={state.text} onChange={handleTextChange} />
      <input value={state.email} onChange={handleEmailChange} type="email" />
      <button type="submit">Submit</button>
    </form>
  );
}

// ============================================
// PART 6: THE TYPED CONTEXT PROVIDER
// ============================================

function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>("light");

  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        Toggle theme
      </button>
      {children}
    </ThemeContext.Provider>
  );
}

// ============================================
// PART 7: THE TYPED CHILDREN CONSUMER
// ============================================

function ThemedBox({ children }: { children: React.ReactNode }) {
  const theme = useTheme();
  return <div className={`box box--${theme}`}>{children}</div>;
}

// ============================================
// PART 8: THE TYPED DISCRIMINATED UNION
// ============================================

type Notification =
  | { type: "success"; message: string }
  | { type: "error"; message: string; retry: () => void }
  | { type: "info"; message: string };

function NotificationBanner({ notification }: { notification: Notification }) {
  if (notification.type === "error") {
    return (
      <div className="error">
        {notification.message}
        <button onClick={notification.retry}>Retry</button>
      </div>
    );
  }
  return <div>{notification.message}</div>;
}

// ============================================
// PART 9: THE TYPED REF
// ============================================

function FocusInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  const focus = () => {
    inputRef.current?.focus();
  };

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focus}>Focus</button>
    </>
  );
}

// ============================================
// PART 10: THE STRICT TSCONFIG
// ============================================

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noImplicitReturns": true,
    "strictFunctionTypes": true
  }
}

The ten parts cover typed props, typed useState, typed useReducer, typed useContext, typed event handlers, a typed context provider, typed children, a discriminated union, a typed ref, and the strict tsconfig.json.


Quick Reference

The Core Types

TypePurpose
React.ReactNodeAnything React can render (children)
React.ChangeEvent<T>Change event for an element
React.FormEvent<T>Form submission event
React.MouseEvent<T>Mouse event for an element
React.Dispatch<React.SetStateAction<S>>Set-state function from useState

The Hooks and Their Inference

HookInfers FromWhen to Add Type Argument
useStateInitial valueUnion types, null starts
useReducerInitial stateDiscriminated union actions
useContextcreateContext valuenull default needs union
useRefGeneric parameterDOM refs: useRef<HTMLInputElement>(null)
useCallbackCallback signatureWhen parameters need explicit types

The Strictness Options

OptionCatches
strict: trueEnables all strict checks
noImplicitAnyUntyped props, handlers, state
strictNullChecksMissing null/undefined handling
noImplicitReturnsMissing return in some branches
strictFunctionTypesMismatched handler signatures

The Ecosystem Integrations

LibraryIntegration
Redux ToolkitRootState, AppDispatch inferred from store
TanStack Querydata inferred from query function
Zodz.infer<typeof schema> for form types

Best Practices

✅ Do This:

// Use .tsx for files with JSX
// App.tsx                                                       // ✅
// Type props with an interface for more than two props
interface ButtonProps { title: string; disabled?: boolean; } // ✅
// Type children with React.ReactNode
function Card({ children }: { children: React.ReactNode }) { ... } // ✅
// Type event handlers with the specific event type
const handle = (e: React.ChangeEvent<HTMLInputElement>) => { ... }; // ✅
// Use discriminated unions for reducer actions
type Action = { type: "increment" } | { type: "decrement" }; // ✅
// Enable strict mode in tsconfig.json
"strict": true                                                // ✅

❌ Don’t Do This:

// Don't use React.FC in new code
const App: React.FC<Props> = ({ title }) => ...;              // ❌ discouraged
// Don't use any for event handlers
const handle = (e: any) => { setEmail(e.target.value); };     // ❌
// Don't leave state type as string when it should be a union
const [status, setStatus] = useState("idle");                 // ❌ too wide
// Don't skip strictNullChecks
"strictNullChecks": false                                     // ❌
// Don't forget .tsx extension for JSX files
// App.ts with JSX syntax                                      // ❌

Common Pitfalls

PitfallWhy It HappensFix
JSX not recognizedWrong file extensionUse .tsx for JSX files
“Cannot find name ‘React'”Missing @types/reactnpm install --save-dev @types/react
event.target.value errortarget is not typed as inputUse React.ChangeEvent<HTMLInputElement>
State type too wideInferred from string initial valueUse type argument: useState<Status>
Context returns nullcreateContext default is nullThrow in the consuming hook
Component returns undefinedMissing return branchEnable noImplicitReturns

Real-World Examples

1. Typed Props Interface

interface ButtonProps { title: string; onClick: () => void; }

2. Typed useState with Union

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

3. Typed useReducer

const [state, dispatch] = useReducer(reducer, initialState);

4. Typed useContext

const ThemeContext = createContext<Theme | null>(null);

5. Typed Change Event

const handle = (e: React.ChangeEvent<HTMLInputElement>) => { ... };

6. Typed Form Event

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { ... };

7. Typed Children

function Card({ children }: { children: React.ReactNode }) { ... }

8. Typed Ref

const inputRef = useRef<HTMLInputElement>(null);

9. Typed Discriminated Union

type Action = { type: "set"; value: string } | { type: "reset" };

10. Strict tsconfig

{ "compilerOptions": { "strict": true } }

Visual

The Props Contract

┌──────────────────────────────────────────────┐
│  PROPS CONTRACT                              │
│                                              │
│  interface ButtonProps {                     │
│    title: string;                            │
│    disabled?: boolean;                       │
│  }                                           │
│                                              │
│  <Button title="Save" disabled={true} />     │
│    └─ TypeScript checks every prop           │
│                                              │
│  <Button titl="Save" />                      │
│    └─ ❌ Error: Property 'titl' does not exist│
│                                              │
│  The interface is the contract.              │
│  The compiler enforces it at every call site.│
│                                              │
└──────────────────────────────────────────────┘

The Event Type Flow

┌──────────────────────────────────────────────┐
│  EVENT TYPE                                  │
│                                              │
│  Without type:                               │
│  const handle = (e) => {                     │
│    e.target.value  // ❌ target is EventTarget│
│  }                                           │
│                                              │
│  With type:                                  │
│  const handle = (                             │
│    e: React.ChangeEvent<HTMLInputElement>    │
│  ) => {                                      │
│    e.target.value  // ✅ target is HTMLInput  │
│  }                                           │
│                                              │
│  The generic parameter specifies the element.│
│                                              │
└──────────────────────────────────────────────┘

The Reducer Action Union

┌──────────────────────────────────────────────┐
│  DISCRIMINATED UNION                         │
│                                              │
│  type Action =                               │
│    | { type: "reset" }                       │
│    | { type: "setCount"; value: number };    │
│                                              │
│  Inside the reducer:                         │
│  if (action.type === "setCount") {           │
│    action.value  // ✅ number                │
│    action.type   // ✅ "setCount"            │
│  }                                           │
│                                              │
│  TypeScript narrows by the discriminant.     │
│  Invalid property access is an error.        │
│                                              │
└──────────────────────────────────────────────┘

The Strictness Payoff

┌──────────────────────────────────────────────┐
│  STRICTNESS OPTIONS                          │
│                                              │
│  strict: true                                │
│    └─ Foundation. Enable all.                │
│                                              │
│  noImplicitAny: true                         │
│    └─ Catches untyped props and handlers.    │
│                                              │
│  strictNullChecks: true                      │
│    └─ Catches missing null handling.         │
│       The most valuable option for React.    │
│                                              │
│  noImplicitReturns: true                     │
│    └─ Catches missing return branches.       │
│                                              │
│  strictFunctionTypes: true                   │
│    └─ Catches mismatched handler signatures. │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Required packages@types/react, @types/react-dom
Required file extension.tsx for JSX files
tsconfig settings"jsx": "preserve", lib: ["dom"]
Props typeinterface or inline type
Children typeReact.ReactNode
Event typeReact.ChangeEvent<T>, React.FormEvent<T>
useState type argumentFor unions and null starts
useReducer patternType the initial state, use discriminated unions
useContext patternnull default with throwing hook
Strict optionsstrict, noImplicitAny, strictNullChecks

Key takeaways:

  • TypeScript understands JSX natively once @types/react and @types/react-dom are installed. The tsconfig.json must set "jsx": "preserve" and include "dom" in the lib array. Files containing JSX must use the .tsx extension .
  • Props are typed with an interface or an inline type. The compiler checks every prop name and type at the call site. The children prop is typed as React.ReactNode .
  • Hooks infer their types from the values passed to them. Type arguments are only needed when the inferred type is too wide — a string that should be a union, or a null that should be T | null .
  • useReducer uses discriminated unions for actions. The action type is a union of objects, each with a type property. TypeScript narrows the action inside each case branch, giving access to the correct properties .
  • Event handlers need explicit event types. React.ChangeEvent<HTMLInputElement> guarantees that e.target is an input element with a value property. Without the type, e.target is EventTarget and value does not exist .
  • The strictness options catch the bugs that matter most. strictNullChecks is the single most valuable option for React. It forces handling of optional props, nullable API responses, and async state that has not resolved yet .
  • The ecosystem integrates with the type system. Redux Toolkit infers RootState and AppDispatch. TanStack Query infers data from the query function. Zod derives types with z.infer. These integrations extend type safety from the component through the data layer .

Remember: TypeScript and React work together because React’s component model is built on object shapes, and TypeScript’s job is to describe object shapes. Props are interfaces. State is a type argument. Events are generic types. The tsconfig.json strict options determine how many mistakes the compiler catches before they reach the browser. Start with strict: true, type your props with interfaces, type your events with the specific React event types, and use discriminated unions for reducer actions. The initial ceremony pays back in editor autocomplete, compile-time error checking, and fewer runtime surprises.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!