TypeScript in React: The Complete Guide

This comprehensive guide covers essential TypeScript concepts for React developers compiled from various YouTube videos and articles. Whether you're new to TypeScript or looking to enhance your React applications with type safety, this guide provides practical examples and explanations to help you get started.

Nish Sitapara
TypeScriptReactTutorial

TypeScript in React: The Complete Guide

This guide represents my journey of learning TypeScript in React, compiled from various sources including YouTube tutorials, documentation, and articles that I found particularly helpful. As I was learning these concepts, I organized them into this comprehensive guide to serve as both a reference for myself and a resource for others. Whether you're new to TypeScript or looking to enhance your React applications with type safety, this guide provides practical examples and explanations that helped me understand and implement TypeScript effectively.

Before we dive in, it's worth noting that TypeScript has become the de facto standard in modern React applications. Through my learning process, I've experienced firsthand how it helps catch errors during development, improves code documentation, and enhances developer experience through better tooling and autocompletion.

Basic TypeScript Concepts

Converting JSX to TSX

To start using TypeScript in a React project, you need to rename your .jsx files to .tsx. This tells TypeScript to treat your files as React components with TypeScript support.

Typing Variables

TypeScript can infer types from variable assignments, but you can also explicitly define types:

// Type inference (preferred when possible)
const url = "https://example.com"; // TypeScript infers this as string

// Explicit typing 
const url: string = "https://example.com";

When you try to reassign a variable with a different type, TypeScript will give you an error:

let url = "https://example.com";
url = 42; // Error: Type 'number' is not assignable to type 'string'

Typing Functions

Function parameters should be typed to prevent errors:

function convertCurrency(amount: number, currency: string): string {
  // Implementation here
  return `${amount} ${currency}`;
}

// Using the function
convertCurrency(100, "USD"); // Works fine
convertCurrency("100", "USD"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'

Typing React Components

Basic Component Typing

In React components, you typically need to type the props:

function Button({ backgroundColor, fontSize, pillShape }: { 
  backgroundColor: string;
  fontSize: number;
  pillShape: boolean;
}) {
  return (
    <button 
      className={`button ${pillShape ? "rounded-full" : ""}`}
      style={{ backgroundColor, fontSize }}
    >
      Click me
    </button>
  );
}

Extracting Types

For cleaner code, extract prop types to a separate type definition:

type ButtonProps = {
  backgroundColor: string;
  fontSize: number;
  pillShape?: boolean; // ? makes this prop optional
};

function Button({ backgroundColor, fontSize, pillShape }: ButtonProps) {
  return (
    <button 
      className={`button ${pillShape ? "rounded-full" : ""}`}
      style={{ backgroundColor, fontSize }}
    >
      Click me
    </button>
  );
}

Union Types

Union types allow for more specific type definitions:

type Color = "red" | "blue" | "green";

type ButtonProps = {
  backgroundColor: Color;
  textColor: Color;
  fontSize: number;
};

Working with Arrays and Objects

Array Types

// Array of numbers
const padding: number[] = [5, 10, 20, 5];

// Alternative syntax
const scores: Array<number> = [85, 90, 95];

Tuple Types

Tuples are arrays with fixed length and specific types:

// A tuple defining padding: [top, right, bottom, left]
const padding: [number, number, number, number] = [5, 10, 20, 5];

Styling with React.CSSProperties

For typing style objects, use React's built-in CSSProperties type:

type ButtonProps = {
  style: React.CSSProperties;
};

function Button({ style }: ButtonProps) {
  return <button style={style}>Click me</button>;
}

// Usage
<Button style={{ backgroundColor: 'red', fontSize: 16 }} />

Record Type

For objects with specific key and value types:

type BorderRadiusProps = {
  borderRadius: Record<string, number>;
};

// Usage
<Button borderRadius={{ topLeft: 5, topRight: 10, bottomRight: 15, bottomLeft: 5 }} />

Typing Event Handlers

Basic Event Handler Types

function Button({ onClick }: { onClick: () => void }) {
  return <button onClick={onClick}>Click me</button>;
}

// With parameters
function Button({ onClick }: { onClick: (test: string) => number }) {
  return <button onClick={() => onClick("test")}>Click me</button>;
}

Event Objects

React automatically types inline event handlers, but extracted functions need explicit typing:

// Inline (automatically typed)
<button onClick={(e) => console.log(e)}>Click me</button>

// Extracted (needs explicit typing)
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  console.log(e);
};

<button onClick={handleClick}>Click me</button>

Children and Component Composition

Typing Children

type ButtonProps = {
  children: React.ReactNode; // Can accept text, elements, or other valid React nodes
};

function Button({ children }: ButtonProps) {
  return <button>{children}</button>;
}

// Usage
<Button>Click me</Button>
<Button><span>Click</span> me</Button>

JSX.Element vs ReactNode

JSX.Element is more restrictive (only elements), while ReactNode accepts text, numbers, elements, etc.:

type RestrictiveProps = {
  children: React.JSX.Element; // Only accepts elements
};

// This works
<RestrictiveComponent><div>Hello</div></RestrictiveComponent>

// This would error
<RestrictiveComponent>Hello</RestrictiveComponent>

React Hooks with TypeScript

useState

TypeScript infers types from initial values, but you can be explicit:

// Type inference from initial value
const [count, setCount] = useState(0); // count is inferred as number

// Explicit typing
const [text, setText] = useState<string>(""); // Explicitly setting type as string

// With complex objects, explicit typing is recommended
type User = {
  name: string;
  age: number;
};

// When initial value is null
const [user, setUser] = useState<User | null>(null);

// Access with optional chaining to handle null
console.log(user?.name);

useRef

// For DOM elements
const buttonRef = useRef<HTMLButtonElement>(null);

// Usage
<button ref={buttonRef}>Click me</button>

Advanced TypeScript Techniques

"as const" Type Assertion

Makes arrays/objects more specific and readonly:

// Without as const
const buttonTextOptions = ["Click me", "Click me again", "Click me one more time"];
// Type is string[]

// With as const
const buttonTextOptions = ["Click me", "Click me again", "Click me one more time"] as const;
// Type is readonly ["Click me", "Click me again", "Click me one more time"]

// Benefits when mapping
buttonTextOptions.map(option => {
  // option is the exact string literal, not just any string
  return <Button key={option}>{option}</Button>
});

Omit Utility Type

Remove properties from an existing type:

type User = {
  name: string;
  sessionId: string;
};

type Guest = Omit<User, "name">;
// Result: { sessionId: string }

Type Assertion with "as"

// When you know more about the type than TypeScript does
const buttonColor = localStorage.getItem("buttonColor") as "red" | "blue" | "green";

Generics

Generics allow you to create reusable components and functions:

// Generic function
function convertToArray<T>(value: T): T[] {
  return [value];
}

// Usage
const numberArray = convertToArray(5); // Type: number[]
const stringArray = convertToArray("hello"); // Type: string[]

// Generic component
type ButtonProps<T> = {
  countValue: T;
  countHistory: T[];
};

function Button<T>({ countValue, countHistory }: ButtonProps<T>) {
  return (
    <button>
      Current: {String(countValue)}, History: {countHistory.join(', ')}
    </button>
  );
}

// Usage
<Button<number> countValue={5} countHistory={[1, 2, 3, 4, 5]} />
<Button<string> countValue="five" countHistory={["one", "two", "three", "four", "five"]} />

Best Practices and Organization

Organizing Types

Create a centralized place for shared types:

// types.ts
export type Color = "red" | "blue" | "green";

// In your component
import { Color } from './types';
// or with type import
import type { Color } from './types';

Using "unknown" Instead of "any"

// Fetching data
useEffect(() => {
  const fetchData = async () => {
    const response = await fetch("https://api.example.com/data");
    const data: unknown = await response.json();
    
    // You must verify the shape before using it
    if (isValidData(data)) {
      // Now it's safe to use
      console.log(data.name);
    }
  };
  
  fetchData();
}, []);

// Type guard function
function isValidData(data: unknown): data is { name: string } {
  return typeof data === 'object' && data !== null && 'name' in data;
}

Configuration

tsconfig.json

Key options to consider:

{
  "compilerOptions": {
    "jsx": "preserve", // How JSX should be transformed
    "strict": true, // Enable all strict type-checking options
    "esModuleInterop": true, // For import compatibility
    "lib": ["dom", "dom.iterable", "esnext"] // Libraries to include
  },
  "include": ["src/**/*"] // Files to include
}

Working with Third-Party Libraries

Most popular libraries have TypeScript definitions available from DefinitelyTyped (@types):

npm install @types/react @types/react-dom

If a library doesn't have types, you can create your own declaration file:

// custom-library.d.ts
declare module 'custom-library' {
  export function doSomething(value: string): number;
}

Conclusion

TypeScript brings significant benefits to React development, including:

  1. Catching errors during development
  2. Better autocompletion and IntelliSense
  3. More maintainable code with explicit contracts
  4. Better refactoring support

While there is a learning curve, the improved developer experience and code quality make it worthwhile for React projects of all sizes. Start with basic typing and gradually adopt more advanced features as you become comfortable with TypeScript.

Remember that TypeScript is designed to help you, not hinder you. When you run into challenges, consider whether the issue is highlighting a genuine problem in your code that would otherwise surface at runtime.

By following the examples in this guide, you'll be able to effectively use TypeScript in your React applications, creating more robust and maintainable code.

Comments

Join the discussion on “TypeScript in React: The Complete Guide

3 Comments

J

John Doe

Nov 15, 2023

Great article! I learned a lot from this.

J
Jane Smith
Nov 15, 2023

I agree! The technical details were very clear.

A

Anonymous

Nov 16, 2023

I have a question about the third point you made. Could you elaborate more on that?

J

Jane Smith

Nov 17, 2023

This is exactly what I was looking for. Thanks for sharing your insights!

A
Anonymous
Nov 17, 2023

Could you share what specifically you found helpful?

J
Jane Smith
Nov 17, 2023

The implementation details in the middle section were exactly what I needed for my project.