DEV Community

Cover image for The Full TypeScript Guide with Easy React Examples
Utkarsh Vishwas
Utkarsh Vishwas

Posted on

The Full TypeScript Guide with Easy React Examples

This guide will take you from the basics of TypeScript to using it effectively in React projects. We'll cover core TypeScript concepts and illustrate them with practical React examples.

Part 1: Introduction to TypeScript and Why Use It?

What is TypeScript?

TypeScript is a superset of JavaScript that adds optional static typing. Think of it as JavaScript plus type safety. It's designed for large applications and makes JavaScript development more robust and maintainable.

Key Differences from JavaScript:

  • Static Typing: TypeScript allows you to define the types of variables, function parameters, and return values. These types are checked during development (compile-time) rather than at runtime.
  • Type Checking: The TypeScript compiler catches type-related errors before you run your code, leading to fewer runtime errors.
  • Improved Code Readability and Maintainability: Types act as documentation, making code easier to understand and refactor.
  • Enhanced IDE Support: TypeScript's type system enables better autocompletion, code navigation, and refactoring tools in your IDE (like VS Code).
  • Early Error Detection: Catch bugs earlier in the development process, saving time and frustration.

Why Use TypeScript, Especially in React?

  • Prevent Common Errors: In React, prop and state management is crucial. TypeScript helps you ensure you are passing the correct data types, preventing common "undefined is not a function" or "cannot read property of undefined" errors.
  • Better Collaboration: In team projects, types provide a clear contract for how components should interact, making collaboration smoother.
  • Refactoring Confidence: When refactoring React components, TypeScript's type system ensures you don't accidentally break existing functionality.
  • Scalability: For larger React applications, TypeScript's structure and type safety become invaluable for managing complexity.
  • Improved Developer Experience: Autocompletion, type hints, and early error detection significantly improve developer productivity in React.

Think of TypeScript as adding seatbelts and airbags to your JavaScript car. It makes your ride safer and more controlled!

Part 2: Core TypeScript Concepts - Building Blocks

Let's explore the essential TypeScript concepts you'll need.

1. Basic Types

TypeScript introduces static types to JavaScript's dynamic nature. Here are the primitive types:

  • boolean: true or false values.
  • number: All numeric values (integers, floats, etc.).
  • string: Textual data.
  • null: Represents the intentional absence of a value.
  • undefined: Represents a variable that has been declared but not yet assigned a value.

Example:

let isDone: boolean = false;
let decimal: number = 6;
let color: string = "blue";
let u: undefined = undefined;
let n: null = null;
Enter fullscreen mode Exit fullscreen mode

2. Arrays

You can define the type of elements an array will hold.

Example:

let list: number[] = [1, 2, 3]; // Array of numbers
let colors: string[] = ['red', 'green', 'blue']; // Array of strings
let mixed: (string | number)[] = ['hello', 10, 'world', 20]; // Array of strings OR numbers
Enter fullscreen mode Exit fullscreen mode

3. Objects

TypeScript allows you to define the shape of objects, specifying the types of their properties.

Example:

let person: { name: string; age: number } = {
  name: "Alice",
  age: 30,
};

// You can also use an interface (more on interfaces later)
interface Person {
  name: string;
  age: number;
}

let anotherPerson: Person = {
  name: "Bob",
  age: 25,
};
Enter fullscreen mode Exit fullscreen mode

4. Functions

Type annotations are powerful in functions. You can specify the types of parameters and the return type.

Example:

// Function with parameter types and return type
function add(x: number, y: number): number {
  return x + y;
}

let result: number = add(5, 3); // result will be of type number

// Void return type (no return value)
function greet(name: string): void {
  console.log(`Hello, ${name}!`);
}

greet("Charlie");
Enter fullscreen mode Exit fullscreen mode

5. Interfaces

Interfaces define contracts for object shapes. They describe the structure an object should have.

Example:

interface User {
  id: number;
  name: string;
  email?: string; // Optional property (using ?)
}

function displayUser(user: User): void {
  console.log(`User ID: ${user.id}, Name: ${user.name}`);
  if (user.email) {
    console.log(`Email: ${user.email}`);
  }
}

let validUser: User = { id: 1, name: "David" };
displayUser(validUser);

let userWithEmail: User = { id: 2, name: "Eve", email: "eve@example.com" };
displayUser(userWithEmail);
Enter fullscreen mode Exit fullscreen mode

Interfaces are extremely important for typing React components and their props.

6. Types (Type Aliases)

Type aliases give a name to a type. They can be used for primitive types, unions, intersections, and more complex types. They are similar to interfaces but can be used more broadly.

Example:

type Point = {
  x: number;
  y: number;
};

let origin: Point = { x: 0, y: 0 };

type StringOrNumber = string | number; // Union type

function processValue(value: StringOrNumber): void {
  if (typeof value === 'string') {
    console.log(`Processing string: ${value.toUpperCase()}`);
  } else {
    console.log(`Processing number: ${value * 2}`);
  }
}

processValue("hello");
processValue(10);
Enter fullscreen mode Exit fullscreen mode

Choosing between Interfaces and Types:

  • Interfaces are primarily for defining object shapes and are generally preferred for public APIs and component props in React. They support declaration merging (you can extend interfaces).
  • Types are more flexible. They can define aliases for primitive types, unions, intersections, and more complex types. If you need to define a type for something that's not an object shape (like a union or primitive), use type.

For React components, interfaces are often the more natural choice for typing props and state.

7. Enums (Enumerations)

Enums are a way to give friendly names to sets of numeric values. They make code more readable when dealing with a fixed set of options.

Example:

enum Status {
  Pending,   // 0 (by default)
  InProgress, // 1
  Completed,  // 2
  Rejected   // 3
}

function getStatusName(status: Status): string {
  switch (status) {
    case Status.Pending:    return "Pending";
    case Status.InProgress: return "In Progress";
    case Status.Completed:  return "Completed";
    case Status.Rejected:   return "Rejected";
    default:             return "Unknown";
  }
}

let currentStatus: Status = Status.InProgress;
console.log(`Current status is: ${getStatusName(currentStatus)}`); // Output: Current status is: In Progress
Enter fullscreen mode Exit fullscreen mode

8. Generics

Generics allow you to write reusable components or functions that can work with various types while maintaining type safety. They are like placeholders for types.

Example (non-React first, then we'll see in React):

// Generic function to return the first element of an array
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

let numbers = [1, 2, 3];
let firstNumber = firstElement<number>(numbers); // T is inferred as number
console.log(firstNumber); // Output: 1

let strings = ["apple", "banana", "cherry"];
let firstString = firstElement<string>(strings); // T is inferred as string
console.log(firstString); // Output: apple

// You can also often let TypeScript infer the generic type:
let anotherFirstNumber = firstElement(numbers); // TypeScript infers <number>
let anotherFirstString = firstElement(strings); // TypeScript infers <string>
Enter fullscreen mode Exit fullscreen mode

Generics are super powerful for creating reusable and type-safe React components, especially for components that handle different types of data.

9. Utility Types (Optional, but Good to Know)

TypeScript provides built-in utility types to transform types in various ways. Some common ones include:

  • Partial<T>: Makes all properties of type T optional.
  • Required<T>: Makes all properties of type T required.
  • Readonly<T>: Makes all properties of type T readonly (cannot be reassigned).
  • Pick<T, K>: Creates a type by picking properties K from type T.
  • Omit<T, K>: Creates a type by omitting properties K from type T.

Example (Illustrative):

interface Config {
  apiUrl: string;
  timeout: number;
  retries: number;
}

type PartialConfig = Partial<Config>; // All properties of Config are now optional
type ReadonlyConfig = Readonly<Config>; // All properties of Config are now readonly

let configUpdate: PartialConfig = { timeout: 5000 }; // Only timeout is needed

// readonlyConfig.apiUrl = "new-api-url"; // Error: Cannot assign to 'apiUrl' because it is a read-only property.
Enter fullscreen mode Exit fullscreen mode

You'll likely encounter utility types in more advanced TypeScript and React scenarios.

Part 3: TypeScript in React - Real-World Examples

Now, let's see how to use TypeScript in React components.

1. Basic React Functional Component with TypeScript

Let's create a simple greeting component.

JavaScript (No TypeScript):

import React from 'react';

function Greeting(props) {
  return <h1>Hello, {props.name}!</h1>;
}

export default Greeting;
Enter fullscreen mode Exit fullscreen mode

TypeScript Version:

import React from 'react';

interface GreetingProps { // Define props interface
  name: string;
}

const Greeting: React.FC<GreetingProps> = (props) => { // Use React.FC and type props
  return <h1>Hello, {props.name}!</h1>;
};

export default Greeting;
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • interface GreetingProps { name: string; }: We define an interface GreetingProps to specify the shape of the props object this component expects. It has a single property name of type string.
  • React.FC<GreetingProps>: React.FC is a generic type from React that represents a Functional Component. We pass GreetingProps as the generic type argument, telling React that this component accepts props that conform to the GreetingProps interface.
  • (props: GreetingProps): Inside the functional component, we explicitly type the props parameter as GreetingProps.

Benefits:

  • Type Safety: If you try to use the Greeting component without passing a name prop or pass a prop of the wrong type (e.g., a number for name), TypeScript will give you a compile-time error.
  • Autocompletion: In your IDE, when you use the Greeting component, you'll get autocompletion for the name prop, and the IDE will know it's supposed to be a string.

2. Typing Props with Interfaces or Types

As we saw, interfaces are great for defining prop types. You can also use type aliases:

Using type for props:

import React from 'react';

type ButtonProps = { // Using type alias for props
  label: string;
  onClick?: () => void; // Optional onClick handler
};

const MyButton: React.FC<ButtonProps> = (props) => {
  return <button onClick={props.onClick}>{props.label}</button>;
};

export default MyButton;
Enter fullscreen mode Exit fullscreen mode

When to use Interface vs. Type for Props:

  • For simple prop shapes, either interface or type works well.
  • Interfaces are often preferred for component props because they are naturally designed for object shapes and can be extended if needed (though you can achieve similar extension with type intersections).
  • If you need to define more complex types for props (like union types, conditional types, etc.), type aliases can be more flexible.

3. Typing State with useState

When using useState in functional components, you can type your state variables:

import React, { useState } from 'react';

interface CounterProps {
  initialCount?: number; // Optional initial count
}

const Counter: React.FC<CounterProps> = (props) => {
  const [count, setCount] = useState<number>(props.initialCount || 0); // Type state as number

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • useState<number>(...): We use the generic type parameter <number> with useState to explicitly tell TypeScript that the count state variable will hold a number.
  • props.initialCount || 0: We provide a default value of 0 if props.initialCount is not provided, and TypeScript knows this initial value is also a number.

Benefits:

  • Type Safety in State Updates: TypeScript ensures you are updating the state with values of the correct type (in this case, numbers).
  • Clear State Definition: The type annotation makes it clear what type of data the state variable holds.

4. Typing Event Handlers

When handling events in React components, you can type your event handler functions and event objects.

import React from 'react';

interface InputProps {
  onInputChange: (value: string) => void; // Function prop to handle input change
}

const InputComponent: React.FC<InputProps> = (props) => {
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { // Type event object
    props.onInputChange(event.target.value); // event.target.value is known to be a string
  };

  return (
    <input
      type="text"
      placeholder="Enter text"
      onChange={handleInputChange}
    />
  );
};

export default InputComponent;
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • (event: React.ChangeEvent<HTMLInputElement>): We type the event parameter in handleInputChange as React.ChangeEvent<HTMLInputElement>. This is a specific type provided by React for change events on HTML input elements. TypeScript now understands the structure of the event object.
  • event.target.value: TypeScript knows that event.target.value in this context will be a string because we've typed the event correctly.

Benefits:

  • Type-Safe Event Handling: TypeScript helps ensure you are accessing the correct properties of the event object and handling event data appropriately.
  • Improved Autocompletion: When working with event objects, TypeScript provides excellent autocompletion for event properties based on the event type.

5. More Complex Example: Fetching Data and Displaying a List

Let's create a component that fetches data from an API and displays a list, using TypeScript.

First, define types for the data:

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

type TodoList = Todo[]; // Type alias for an array of Todos
Enter fullscreen mode Exit fullscreen mode

React Component:

import React, { useState, useEffect } from 'react';

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

type TodoList = Todo[];

const TodoListComponent: React.FC = () => { // No props for this component
  const [todos, setTodos] = useState<TodoList>([]); // State is TodoList (array of Todos)
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null); // Error can be string or null

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/todos');
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data: TodoList = await response.json(); // Type the fetched data as TodoList
        setTodos(data);
      } catch (e: any) { // Type catch error as 'any' or more specific Error type
        setError(e.message || "An error occurred");
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []);

  if (loading) {
    return <p>Loading todos...</p>;
  }

  if (error) {
    return <p>Error fetching todos: {error}</p>;
  }

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          {todo.title} {todo.completed ? "(Completed)" : "(Pending)"}
        </li>
      ))}
    </ul>
  );
};

export default TodoListComponent;
Enter fullscreen mode Exit fullscreen mode

Key TypeScript Aspects:

  • interface Todo and type TodoList: Define the structure of the data we expect from the API.
  • useState<TodoList>([]);: Type the todos state to be TodoList, ensuring it holds an array of Todo objects.
  • useState<boolean>(true); and useState<string | null>(null);: Type loading and error states. error can be either a string (error message) or null (no error).
  • const data: TodoList = await response.json();: Type the data received from response.json() as TodoList, ensuring type consistency.
  • catch (e: any): In the catch block, we type the error e as any (or you could use a more specific Error type if you know the error structure better). This allows us to access e.message.

Benefits:

  • Data Integrity: TypeScript ensures that the data fetched from the API conforms to the Todo and TodoList types. If the API response structure changes unexpectedly, TypeScript will likely catch type errors.
  • Clear State Management: The types for todos, loading, and error make the component's state logic easier to understand and maintain.
  • Error Handling Clarity: Typing the error state as string | null makes it explicit that it can hold an error message string or be null when there's no error.

Part 4: Benefits Recap and Going Further

Recap of TypeScript Benefits in React

  • Early Bug Detection: Catch type-related errors during development, before runtime.
  • Improved Code Readability: Types serve as documentation, making code easier to understand.
  • Enhanced Maintainability: Refactor with confidence, knowing TypeScript will help prevent unintended breakages.
  • Better Collaboration: Clear type contracts improve teamwork.
  • Scalability: Manage complexity in larger React applications.
  • Superior Developer Experience: Autocompletion, type hints, and early error feedback boost productivity.

Going Further

This guide has covered the essentials. To deepen your TypeScript knowledge for React:

  • Explore more advanced React types: Learn about React.ContextType, React.Ref, React.ReactNode, React.ReactElement, etc.
  • Dive into more complex generic patterns: Explore using generics in custom hooks, higher-order components, and more reusable components.
  • Learn about conditional types and mapped types: These allow you to create more dynamic and flexible types.
  • Configure tsconfig.json effectively: Understand TypeScript compiler options to fine-tune your project's type checking and compilation process.
  • Integrate linters and formatters: Use ESLint with TypeScript and Prettier to maintain code quality and consistency.
  • Practice! The best way to learn is to build React projects with TypeScript. Start small and gradually increase complexity.

Top comments (0)