CodeFixesHub
    programming tutorial

    Typing Class Components in React with TypeScript (Basic)

    Learn to type React class components: props, state, refs, lifecycle, and event handlers with examples and debugging tips. Start typing confidently today.

    article details

    Quick Overview

    TypeScript
    Category
    Sep 30
    Published
    20
    Min Read
    2K
    Words
    article summary

    Learn to type React class components: props, state, refs, lifecycle, and event handlers with examples and debugging tips. Start typing confidently today.

    Typing Class Components in React with TypeScript (Basic)

    Introduction

    Class components still appear in many codebases: legacy projects, libraries, and apps where migration to hooks is incomplete. If you work on such projects or maintain components shared across teams, understanding how to type class components in TypeScript is essential. This guide covers concrete patterns for typing props, state, refs, lifecycle methods, event handlers, default props, and integration with JavaScript libraries. You will also learn how to avoid common pitfalls such as incorrect this typing, unsafe setState usage, and noisy compiler errors.

    In this tutorial you'll learn how to:

    • Correctly declare Props and State types and use them with React.Component and React.PureComponent.
    • Type event handlers, refs, and lifecycle methods safely.
    • Use TypeScript utility types to make components more robust and maintainable.
    • Troubleshoot common compiler errors and apply recommended tsconfig strictness settings for predictable behavior.

    We'll walk through realistic code examples, both minimal and slightly advanced, and provide debugging tips and references to further reading so you can adopt these patterns in your codebase with confidence.

    For a broader view of common compiler issues and how they interact with component typing, keep our reference on Common TypeScript Compiler Errors Explained and Fixed handy while following the examples.

    Background & Context

    React has moved toward function components and hooks, but class components remain valid and are sometimes required: lifecycle granularity, legacy HOCs, or library consumers that expect class-based components. TypeScript's strong type system helps prevent bugs and provides editor tooling, but class components introduce unique typing considerations — particularly around this, lifecycle method signatures, and setState. Clear, consistent typing reduces runtime surprises and improves refactoring safety.

    Typing patterns for class components are similar to other TypeScript features: create explicit interfaces for external contracts (props), use generics for component base classes, and prefer narrow, immutable types for state when possible. If you need to organize larger apps or libraries, review Organizing Your TypeScript Code: Files, Modules, and Namespaces to align your component typing with module boundaries. Also consider consistent style via Naming Conventions in TypeScript (Types, Interfaces, Variables) to keep your types discoverable.

    Key Takeaways

    • Use interfaces or type aliases to define Props and State explicitly.
    • Pass types to React.Component as generics: React.Component<Props, State>.
    • Type event handlers using React's synthetic event types to avoid runtime mismatches.
    • Use RefObject and React.createRef() for typed refs.
    • When using setState, prefer updater form and typed partial updates.
    • Prefer small, focused state shapes and consider Readonly for immutability.

    Prerequisites & Setup

    Before you follow the examples, ensure your environment meets these requirements:

    • Node.js and npm/yarn installed.
    • A React + TypeScript project (create-react-app with --template typescript or a custom setup).
    • tsconfig configured with at least strictNullChecks and noImplicitAny to catch common mistakes. For recommended flags and migration tips see Recommended tsconfig.json Strictness Flags for New Projects.
    • Familiarity with basic React and TypeScript syntax — generics, interfaces, and union/utility types.

    If you're incrementally adopting TypeScript in a JavaScript codebase, read our step-by-step migration guide at Migrating a JavaScript Project to TypeScript (Step-by-Step) before converting many class components at once.

    Main Tutorial Sections

    1) Basic Props and State Typing

    Start by declaring explicit types for props and state. Use interface or type alias depending on preference. Pass them as generics to React.Component.

    tsx
    import React from 'react';
    
    interface CounterProps {
      initialCount?: number; // optional prop
      label?: string;
    }
    
    interface CounterState {
      count: number;
    }
    
    class Counter extends React.Component<CounterProps, CounterState> {
      state: CounterState = { count: this.props.initialCount ?? 0 };
    
      render() {
        return (
          <div>
            <span>{this.props.label ?? 'Count'}: {this.state.count}</span>
          </div>
        );
      }
    }

    Notes: React.Component<Props, State> is the canonical form. If you omit State, pass {} or undefined explicitly.

    2) Default Props and Optional Props

    Handling defaultProps in TypeScript for class components requires care so the type system understands the runtime default. One common approach uses a static defaultProps declaration and keeps prop types optional where defaults exist.

    tsx
    interface ButtonProps {
      text?: string; // optional because defaultProps supplies it
      onClick?: () => void;
    }
    
    class Button extends React.Component<ButtonProps> {
      static defaultProps = {
        text: 'Click me',
      };
    
      render() {
        return <button onClick={this.props.onClick}>{this.props.text}</button>;
      }
    }

    Tip: Keep defaultProps minimal and prefer explicit caller-provided props where possible. For naming consistency see Naming Conventions in TypeScript (Types, Interfaces, Variables).

    3) Typing Event Handlers

    Event handlers should use React's synthetic event types to reflect DOM event shapes. Use the correct generic for the element, e.g. React.MouseEvent.

    tsx
    class Clicker extends React.Component {
      handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
        e.preventDefault();
        console.log('clicked', e.currentTarget);
      };
    
      render() {
        return <button onClick={this.handleClick}>Click</button>;
      }
    }

    For a deeper dive into event types and Node vs DOM differences, consult Typing Events and Event Handlers in TypeScript (DOM & Node.js).

    4) Typing Refs in Class Components

    Refs are often used inside class components. Use React.createRef() and RefObject for typed refs. This eliminates many runtime casting needs.

    tsx
    class TextInput extends React.Component {
      inputRef: React.RefObject<HTMLInputElement> = React.createRef();
    
      focus() {
        // safe because inputRef.current is typed
        this.inputRef.current?.focus();
      }
    
      render() {
        return <input ref={this.inputRef} />;
      }
    }

    When integrating with third-party UI libraries, read Using JavaScript Libraries in TypeScript Projects for declaration strategies.

    5) Typing Lifecycle Methods and this

    Lifecycle methods like componentDidMount and shouldComponentUpdate have specific signatures. Rely on the definitions in @types/react rather than hand-crafting them.

    tsx
    interface TimerState { seconds: number }
    
    class Timer extends React.Component<{}, TimerState> {
      state: TimerState = { seconds: 0 };
    
      componentDidMount() {
        this._interval = window.setInterval(() => this.tick(), 1000);
      }
    
      componentWillUnmount() {
        window.clearInterval(this._interval);
      }
    
      tick() {
        this.setState(prev => ({ seconds: prev.seconds + 1 }));
      }
    }

    If you ever get confusing errors about calling methods or incorrect this, see guidance at Fixing the "This expression is not callable" Error in TypeScript.

    6) Correctly Typing setState

    setState accepts either a partial object or an updater. TypeScript's typing of setState is helpful but you should prefer the updater when new state depends on previous state.

    tsx
    this.setState({ count: 10 });
    // updater form
    this.setState(prev => ({ count: prev.count + 1 }));

    Use careful typing for nested state; consider normalizing or using immutable structures. Also consider Readonly wrappers where appropriate — see Using Readonly vs. Immutability Libraries in TypeScript for trade-offs.

    7) Typing Callbacks and Passing Handlers Down

    When class components pass event handlers or callbacks to children, type those function shapes explicitly so callers get accurate autocompletion.

    tsx
    type OnSelect = (id: string) => void;
    
    interface ListProps { onSelect: OnSelect }
    
    class List extends React.Component<ListProps> {
      handleClick = (id: string) => {
        this.props.onSelect(id);
      };
      // ... render items with onClick={() => this.handleClick(item.id)}
    }

    If you need patterns and generics for callbacks, see Typing Callbacks in TypeScript: Patterns, Examples, and Best Practices.

    8) Async Methods Inside Class Components

    Class methods can be async. Type their return values carefully and handle Promises where necessary.

    tsx
    class Loader extends React.Component {
      async load() {
        const data = await fetch('/api/data').then(res => res.json());
        this.setState({ data });
      }
    
      componentDidMount() {
        this.load().catch(err => console.error(err));
      }
    }

    Always type the resolved value from Promises; consult Typing Asynchronous JavaScript: Promises and Async/Await for patterns to avoid implicit any.

    9) Integrating with Plain JavaScript and Declaration Files

    When a class component uses a JS library without types, provide minimal declarations or augment module types. Prefer writing .d.ts files or using declare module where necessary.

    ts
    // global.d.ts
    declare module 'old-js-lib';

    See Calling JavaScript from TypeScript and Vice Versa: A Practical Guide and Using JavaScript Libraries in TypeScript Projects for robust strategies.

    10) Common Compiler Errors and How to Fix Them

    When typing class components you'll encounter errors like "property does not exist on type" or "argument of type X is not assignable to parameter Y". Read targeted fixes and patterns at Property 'x' does not exist on type 'Y' Error: Diagnosis and Fixes and Resolving the 'Argument of type 'X' is not assignable to parameter of type 'Y'' Error in TypeScript to quickly diagnose and fix them.

    Practical debugging tips:

    Advanced Techniques

    Once you're comfortable with the basics, adopt these patterns to scale typing in larger codebases. Use advanced generics to create HOCs that preserve prop types, or use mapped and utility types (Partial, Readonly, Pick, Omit) to transform prop and state shapes without duplicating definitions. For example, if you wrap a component with a HOC that injects props, use Omit to prevent prop collision:

    ts
    type Injected = { i18n: I18n };
    function withI18n<P extends Injected>(Component: React.ComponentType<P>) {
      return class extends React.Component<Omit<P, keyof Injected>> {
        render() {
          const injected: Injected = { i18n: createI18n() };
          return <Component {...(this.props as P)} {...injected} />;
        }
      };
    }

    Also consider immutability strategies for state: shallow immutable state is easier to type and reason about. Compare using built-in Readonly types vs dedicated libraries in Using Readonly vs. Immutability Libraries in TypeScript. Finally, adopt stricter tsconfig flags for libraries to encourage better consumer ergonomics and fewer surprises.

    Best Practices & Common Pitfalls

    Dos:

    • Define clear Props and State interfaces and reuse them across tests and docs.
    • Prefer small state shapes and immutable updates.
    • Type event handlers and refs explicitly for accurate tooling.
    • Use updater form of setState when deriving new state from previous state.
    • Keep defaultProps minimal and prefer explicit values where clarity is needed.

    Don'ts:

    • Avoid overusing any or leaving props implicitly typed — this defeats TypeScript’s benefit.
    • Don't coerce types with as unless you are certain (and document why).
    • Avoid mixing too many responsibilities in one component — split concerns and organize files using patterns in Organizing Your TypeScript Code: Files, Modules, and Namespaces.

    Troubleshooting tips:

    • If the compiler complains about missing properties, cross-check whether a prop is optional or whether defaultProps should be applied.
    • Use the verbose errors to trace the origin; if errors are confusing, enabling more strict tsconfig flags (see Recommended tsconfig.json Strictness Flags for New Projects) often reveals the root cause.

    Real-World Applications

    Typing class components is useful in several real-world scenarios:

    Concrete example: a form component that needs typed onSubmit, typed refs to inputs, and clear prop typings will reduce edge-case bugs when other teams consume your component.

    Conclusion & Next Steps

    Typing React class components in TypeScript is manageable when you follow consistent patterns: define Props/State, use React's typed events and refs, prefer updater setState, and adopt strict compiler flags. While function components with hooks are increasingly common, class components remain relevant in many codebases. Next, practice by typing a few components in your repo and run the TypeScript compiler with stricter flags. For broader code hygiene, review Best Practices for Writing Clean and Maintainable TypeScript Code to consolidate patterns across your project.

    Enhanced FAQ

    Q: Should I still use class components in new projects? A: Prefer function components with hooks for new projects due to simpler composition and smaller bundle sizes. However, class components are still supported and useful when migrating legacy code or when libraries require them.

    Q: How do I type setState for partial updates to nested objects? A: For nested state, prefer the updater form and avoid mutating nested values directly. If you have a nested shape, consider splitting nested pieces into separate fields or using immutable utilities. Example:

    ts
    this.setState(prev => ({ nested: { ...prev.nested, value: newValue } }));

    Also consider using utility types to type partial updates more strictly.

    Q: How do I ensure defaultProps are recognized by TypeScript? A: With class components, define static defaultProps and keep the corresponding prop fields optional in your prop type. Alternatively, some prefer to avoid defaultProps and require callers to pass values explicitly. If you mix defaultProps with strict typing, check that your TypeScript version and lib definitions support the inference you expect.

    Q: Why do I get "Property 'x' does not exist on type 'Y'" when accessing this.props.x? A: That usually means your Props type didn't declare x or you used the wrong prop type on the component. Verify your component's Props interface and how the component is being used. See Property 'x' does not exist on type 'Y' Error: Diagnosis and Fixes for step-by-step fixes.

    Q: What's the best way to type refs for components from third-party libraries? A: If the library provides types, use their exported types for refs; otherwise, create minimal declaration files or cast the ref to the expected type cautiously. See Using JavaScript Libraries in TypeScript Projects for strategies.

    Q: How do I type an event handler that can receive different element types? A: Use generic event types and widen using union types if necessary. Example: (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void. For complex event typing, consult Typing Events and Event Handlers in TypeScript (DOM & Node.js).

    Q: How does typing change when converting a class component to a function component? A: Types move from React.Component<Props, State> to function signatures using Props only. State is handled with useState hooks which accept type parameters, e.g. useState(0). The migration guide at Migrating a JavaScript Project to TypeScript (Step-by-Step) can help plan conversions while preserving typing.

    Q: I'm seeing "Argument of type 'X' is not assignable to parameter of type 'Y'" from setState. What do I do? A: This indicates your supplied object doesn't conform to the expected state shape. Use the updater form to ensure types line up, or explicitly cast/transform input values to the correct types. The article Resolving the 'Argument of type 'X' is not assignable to parameter of type 'Y'' Error in TypeScript gives concrete examples and fixes.

    Q: Any performance tips for class components in TypeScript? A: Use PureComponent or implement shouldComponentUpdate carefully to avoid unnecessary renders. Keep component props and state small and immutable to allow fast comparisons. When using heavy typed logic, avoid creating functions in render — instead bind or use class fields so function identity remains stable.

    Q: Where can I learn more about organizing typed React code at scale? A: After mastering component typing, explore Organizing Your TypeScript Code: Files, Modules, and Namespaces and Best Practices for Writing Clean and Maintainable TypeScript Code to scale patterns across larger projects.

    article completed

    Great Work!

    You've successfully completed this TypeScript tutorial. Ready to explore more concepts and enhance your development skills?

    share this article

    Found This Helpful?

    Share this TypeScript tutorial with your network and help other developers learn!

    continue learning

    Related Articles

    Discover more programming tutorials and solutions related to this topic.

    No related articles found.

    Try browsing our categories for more content.

    Content Sync Status
    Offline
    Changes: 0
    Last sync: 11:19:51 PM
    Next sync: 60s
    Loading CodeFixesHub...