TypeScript with React, Redux, and Hooks

In this article, we will set up a ReactJS project with TypeScript support and discuss how to handle type checking for components, hooks, and events.

1. Creating a React Project with TypeScript

We can easily create a TypeScript-based React project using the following command:

npx create-react-app learn-typescript --template typescript

This command creates the project structure with a tsconfig.json file and uses .tsx extensions for React components instead of .js.


2. Defining Functional Components

There are two common ways to define functional components in TypeScript.

Method 1: Using Type Inference for Props

interface ChildProps {
  color: string;
  children?: React.ReactNode;
}

const Child = ({ color }: ChildProps) => {
  return <p style=>Child Component</p>;
};

const Parent = () => {
  return <Child color="red" />;
};

In this case, Child is just a standard JavaScript function that takes arguments matching the ChildProps interface.

Method 2: Using React.FC

interface ChildProps {
  color: string;
}

const Child: React.FC<ChildProps> = ({ color, children }) => {
  return (
    <div>
        <p style=>Child Component</p>
        {children}
    </div>
  );
};

With React.FC (Functional Component), TypeScript automatically includes the children prop (in older versions of React types) and provides types for properties like defaultProps and displayName.

Note: As of React 18, children is no longer included implicitly in React.FC. best practice suggests defining props explicitly (Method 1) or adding children to your interface manually.


3. Typing useState Hook

// TypeScript infers 'string'
const [state, setState] = useState("");

In the code above, TypeScript automatically infers the type of state as string. However, for arrays or complex objects, we need to be explicit.

// Explicitly setting type as string array
const [items, setItems] = useState<string[]>([]);

Union Types in State

Sometimes a state can be null, undefined, or an object. We can use union types for this:

interface User {
    name: string;
    age: number;
}

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

4. Typing Events

Handling events like onChange or onSubmit requires specific React event types.

Inline Handlers (Inferred)

return (
  <form>
      {/* TypeScript infers the event type here automatically */}
    <input onChange={(e) => console.log(e.target.value)} name="demoField" />
  </form>
);

Defined Handlers (Explicit)

When defining the handler outside JSX, you must specify the event type.

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

return (
  <form>
    <input onChange={handleChange} name="demoField" />
  </form>
);

Common Event Types:

  • React.ChangeEvent<HTMLInputElement>
  • React.FormEvent<HTMLFormElement>
  • React.MouseEvent<HTMLButtonElement>

5. TypeScript with Redux (Reducer Example)

When using Redux with TypeScript, we need to define types for our State and Actions.

// 1. Define the State Interface
interface CounterState {
  count: number;
}

// 2. Define Action Types
type Action = 
  | { type: 'INCREMENT' } 
  | { type: 'DECREMENT' } 
  | { type: 'RESET' };

const initialState: CounterState = { count: 0 };

// 3. Create the Reducer with types
const counterReducer = (state: CounterState = initialState, action: Action): CounterState => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return { count: 0 };
    default:
      return state;
  }
};

Using Redux Toolkit (recommended) simplifies this greatly as it auto-generates types, but understanding the underlying type structure is useful.