DEV Community

Cover image for How To Migrate a React Project from JavaScript to TypeScript
Leandro Nuñez for Digital Pollution

Posted on • Updated on

How To Migrate a React Project from JavaScript to TypeScript

Also wrote here

Table of contents

Introduction

Why Migrate? Understanding the Benefits

Before You Start: Prerequisites

Initiating the Migration: Setting Up TypeScript in Your Project

Refactoring React Components

State Management and Context API

Routing and Async Operations

Testing in TypeScript

Handling Non-TypeScript Packages

Best Practices and Common Pitfalls

Conclusion

Additional Resources


Introduction

Hey there, fellow developers! It's exciting to see you here, ready to explore the transition from JavaScript to TypeScript in our React projects.

Now, if you've worked with JavaScript, you know it's like that old, comfortable pair of shoes - a little worn, sometimes unpredictable, but familiar.

TypeScript, however, is like getting a shoe upgrade with custom insoles; it's the same walking experience but with extra support.

So, what's all the buzz about TypeScript? Well, it's essentially JavaScript but with a good dose of extra capabilities thrown in, the most significant being type checking.

Imagine coding without those pesky undefined is not a function errors appearing out of the blue. That's the kind of peace TypeScript brings to your life.

In this guide, we're walking through the why and the how of integrating TypeScript into your React project.

Why React? Because it's awesome and we love it, obviously. But also, combining React's component-based approach with TypeScript’s type-checking features makes for a seriously efficient and enjoyable coding experience.

Here's a sneak peek of what adding TypeScript to a project looks like. In a typical JavaScript component, you'd have:

// JavaScript way
function Greeting({ name }) {
  return <h1>Hello, {name}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

With TypeScript, we're introducing a way to ensure name is always treated as a string:

// TypeScript style
type Props = {
  name: string;
};

function Greeting({ name }: Props) {
  return <h1>Hello, {name}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

Notice the type Props part? That's TypeScript’s way of saying, "Hey, I'm watching; better make sure name is a string!" It's a simple change with profound implications. You now have a guardian angel actively preventing type-related bugs, making your code more robust and predictable.

But that's just a tiny glimpse. There's a whole world of benefits and practices with TypeScript that we'll unpack in this comprehensive guide. From setting up your environment to refactoring components and props, and even best practices to avoid common pitfalls, we've got a lot to cover. So, buckle up, and let's get this show on the road!

Back to Table of Contents


Why Migrate? Understanding the Benefits

If you're contemplating the shift from JavaScript to TypeScript, especially in your React projects, you're not alone in wondering, "Is it genuinely worth the hassle?" Transitioning an entire project's language is no small feat; it requires effort, learning, and, initially, a bit of slowed-down productivity. So, why do developers make the switch? Let's break down the compelling reasons.

1. Catch Errors Sooner: Static Type Checking

The core feature of TypeScript is its static type system. Unlike JavaScript, which is dynamically typed, TypeScript allows you to specify types for your variables, function parameters, and returned values. What's the perk? Errors are caught during development, long before the code gets anywhere near production.

Consider a simple example:

// In JavaScript
function createGreeting(name) {
  return `Hello, ${name}`;
}

// You might not catch this typo until runtime
const greeting = createGreeting(123);
console.log(greeting);  // "Hello, 123" - Wait, that's not right!
Enter fullscreen mode Exit fullscreen mode

Now, let's see how TypeScript helps:

// In TypeScript
function createGreeting(name: string): string {
  return `Hello, ${name}`;
}

// TypeScript will flag this immediately - '123' is not a string!
const greeting = createGreeting(123);
Enter fullscreen mode Exit fullscreen mode

With TypeScript, that innocent-looking bug would've been caught instantly, ensuring that you're aware of the mishap the moment it occurs. This way, the feedback loop is shortened, and you're not left scratching your head looking at strange bugs in your production environment.

2. Improve Code Quality and Understandability

TypeScript's enforcement of typing means that any other developer (or even future you) can understand at a glance what kind of data a function expects and what it returns. This clarity makes codebases more readable and self-documenting.

Imagine coming across a JavaScript function written by a colleague:

function calculateTotal(items) {
  // ... complicated logic ...
}
Enter fullscreen mode Exit fullscreen mode

You'd probably need to dig through the function or find where it's used to understand what items should be. With TypeScript, it's immediately clear:

type Item = {
  price: number;
  quantity: number;
};

// Now we know exactly what to expect!
function calculateTotal(items: Item[]): number {
  // ... complicated logic ...
}
Enter fullscreen mode Exit fullscreen mode

3. Enhanced Editor Support

TypeScript takes the developer experience to a new level by enhancing text editors and IDEs with improved autocompletion, refactoring, and debugging. This integration is possible because TypeScript can share its understanding of your code with your editor.

You'll experience this when you find your editor suggesting method names, providing function parameter info, or warning you about incorrect function usage. It's like having a co-pilot who helps navigate through the code with an extra layer of safety.

4. Easier Collaboration

In a team environment, TypeScript shines by helping enforce certain standards and structures across the codebase. When multiple developers contribute to a project, TypeScript’s strict rules ensure everyone adheres to the same coding guidelines, making collaboration smoother. It's a common language that speaks 'quality and consistency' across the board.

5. Future-Proofing Your Code

JavaScript is evolving, and TypeScript aims to be abreast of the latest features. By using TypeScript, you can start leveraging the next generation of JavaScript features before they hit mainstream adoption, ensuring your project stays modern and cutting-edge.

In conclusion, migrating to TypeScript isn't just about catching errors earlier; it’s about a holistic improvement of your coding process. From better team collaboration to future-proofing your projects, TypeScript provides a robust foundation for building reliable, scalable, and maintainable applications.

Making the switch might seem daunting at first, but with the benefits laid out above, it’s clear why TypeScript has become a favorite for many developers worldwide. Ready to dive in? Let's proceed!

Back to Table of Contents


Before You Start: Prerequisites

Alright, so you're all geared up to make the switch to TypeScript with your React project? Great decision!

But before we dive into the actual process, we need to make sure a few things are in place.

Consider this our prep stage, where we get all our tools ready so that the transition process is as smooth as butter.

Here's what you need to have ready:

1. Existing React Project

First things first, you need an existing React project. This project should be one you're comfortable experimenting with; while the migration process is quite straightforward, you'll want to do this in a space where it's okay to make temporary messes.

// Here's a simple React functional component in your project
export default function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}
Enter fullscreen mode Exit fullscreen mode

This component is a good starting point - it's functional, it's clean, and we can see what's going on at a glance.

2. Basic Understanding of TypeScript

You don’t need to be a TypeScript guru, but understanding the basics will make this transition a whole lot easier.

Know how to define types, interfaces, and know the difference between type and interface.

A little homework goes a long way, trust me.

// A sneak peek into TypeScript syntax
type Props = {
  name: string;  // defining the 'name' expected to be a string
};

// Your component in TypeScript would look like this
import React, { FC } from 'react';

interface GreetingProps {
  name: string;
}

const Greeting: FC<GreetingProps> = ({ name }) => {
  return <h1>Hello, {name}!</h1>;
}

export default Greeting;
Enter fullscreen mode Exit fullscreen mode

See the difference? We're now being explicit about what Greeting expects, making our component more predictable and easier to work with.

3. Node and NPM/Yarn

Your development environment should have Node.js installed because we're going to use npm or yarn for handling our packages. This requirement is a given since you're working with React, but no harm in making sure, right?

# Check if Node is installed
node --version

# Check if npm is installed
npm --version

# Or for yarn
yarn --version
Enter fullscreen mode Exit fullscreen mode

Your terminal should show you the current versions of these tools, confirming they're all set up and ready to go.

4. Code Editor

You're going to need a code editor that can handle TypeScript well. Visual Studio Code is a crowd favorite because it has robust TypeScript support built-in, making the development process smoother with intelligent code completion and error highlighting.

5. Version Control

This step isn't mandatory, but it's a smart one. Make sure your current project is under version control with git. If anything goes sideways (though we'll try to ensure it doesn't), you can always revert to a previous version without losing sleep.

# Check if git is installed
git --version

# If not, you need to initialize version control before proceeding
git init
git add .
git commit -m "Pre-TypeScript commit"
Enter fullscreen mode Exit fullscreen mode

Having this safety net means you can experiment with confidence, knowing your back is covered.

That's about it for our prerequisites! You've got the project, brushed up on some TypeScript, your environment is ready, and your safety net is in place.

Now, we're all set to dive into the migration process. Let's get the ball rolling!

Back to Table of Contents


Initiating the Migration: Setting Up TypeScript in Your Project

Great, you're still with me!

Now that we're prepped, it's time to roll up our sleeves and start the actual work.

We're going to set up TypeScript in our React project.

This stage is like setting up a new workspace where, instead of paint and brushes, we prepare our coding environment with the right tools and configurations.

Follow along, and let's do this step by step.

1. Installing TypeScript

First off, we need to bring TypeScript into our project. We're installing the TypeScript compiler here, so our project knows how to handle the .ts and .tsx files we'll add later.

# Using npm
npm install --save-dev typescript

# Using yarn
yarn add typescript --dev
Enter fullscreen mode Exit fullscreen mode

This command adds TypeScript as a development dependency to your project. It's not something that your users will need, but your development environment certainly will!

2. TypeScript Configuration File

With TypeScript installed, we need to add a configuration file for it. This file is the tsconfig.json, and it's super important because it tells TypeScript how to behave.

Let's create this file:

# In the root of your project, run:
npx tsc --init
Enter fullscreen mode Exit fullscreen mode

This command initializes a very handy tsconfig.json file with default settings that you can adjust according to your project’s needs. Open that file, and you'll see a bunch of configuration options commented out. For a React project, you'll want to uncomment or add some specific lines:

{
  "compilerOptions": {
    "target": "es5",                          
    "lib": ["dom", "dom.iterable", "esnext"], 
    "allowJs": true,                          
    "skipLibCheck": true,                     
    "esModuleInterop": true,                  
    "allowSyntheticDefaultImports": true,     
    "strict": true,                           
    "forceConsistentCasingInFileNames": true, 
    "module": "esnext",                       
    "moduleResolution": "node",               
    "resolveJsonModule": true,                
    "isolatedModules": true,                  
    "noEmit": true,                           
    "jsx": "react-jsx"                        
  },
  "include": ["src"]                          
}
Enter fullscreen mode Exit fullscreen mode

Here, we're setting up some ground rules for TypeScript. For instance, "jsx": "react-jsx" tells TypeScript we're working with JSX (thanks to React), and "include": ["src"] lets TypeScript know where our source files are located.

3. Installing TypeScript Types for React

Now, because we're using TypeScript with React, we need to ensure TypeScript understands the types coming from the React and ReactDOM libraries. We'll need to install these types as development dependencies.

# Using npm
npm install --save-dev @types/react @types/react-dom

# Using yarn
yarn add --dev @types/react @types/react-dom
Enter fullscreen mode Exit fullscreen mode

These are DefinitelyTyped packages, and they're lifesavers because they contain all the type definitions for React and ReactDOM. This way, TypeScript can understand what's happening when we use React-specific syntax and structures.

4. Adjusting React Scripts (if created with Create React App)

If your project bootstrapped with Create React App, there's one more thing to do. We need to let CRA know that we're working with TypeScript now.

# Using npm
npm install --save-dev react-scripts@latest

# Using yarn
yarn add --dev react-scripts@latest
Enter fullscreen mode Exit fullscreen mode

This step ensures that the underlying scripts and tools used by Create React App are up to date and can handle TypeScript files properly.

5. Renaming Files

Here's where things start to look a little different. It's time to rename our .js files to .tsx (or .ts if they're not React components).

// Rename your component files like so:
Component.js -> Component.tsx
Enter fullscreen mode Exit fullscreen mode

This change tells our project, "Hey, we're in TypeScript territory now!" It's a clear signal that we're working with a typed superset of JavaScript, not plain JavaScript.

And there we have it! We've successfully set up TypeScript in our React project. It wasn't so tough, right?

We've laid the groundwork necessary to start taking advantage of TypeScript's features.

Now, let's move forward with transforming our actual code!

Back to Table of Contents


Refactoring React Components

Alright, onto the next phase!

We've set the stage with TypeScript, but now we've got to get our hands dirty.

It's time to refactor our React components. This step involves a bit more than just changing file extensions; we need to update our component code to utilize TypeScript's features for a more robust, error-free coding experience.

Let's dive in!

1. Changing File Extensions

First things first, let's rename our component files. This process involves changing the extension from .js to .tsx for files that contain JSX code. Here's how you can do this en masse in your project's source directory from the command line:

# For Unix-like shells, navigate to your source folder and run:
find . -name "*.js" -exec bash -c 'mv "$0" "${0%.js}.tsx"' {} \;

# If you're using PowerShell (Windows), navigate to your source folder and run:
Get-ChildItem -Filter *.js -Recurse | Rename-Item -NewName { $_.Name -replace '\.js$','.tsx' }
Enter fullscreen mode Exit fullscreen mode

These commands search for all .js files in your project's source directory and rename them to .tsx. It's like telling your files, "Welcome to the TypeScript world!"

2. Typing Your Components

With our files renamed, let's tackle the code. We'll start with a simple functional component in JavaScript:

// Before: MyComponent.js
import React from 'react';

function MyComponent({ greeting }) {
  return <h1>{greeting}, world!</h1>;
}
Enter fullscreen mode Exit fullscreen mode

Now, let’s refactor this to use TypeScript:

// After: MyComponent.tsx
import React, { FC } from 'react';

// Define a type for the component props
interface MyComponentProps {
  greeting: string;
}

// Use the 'FC' (Functional Component) generic from React, with our props type
const MyComponent: FC<MyComponentProps> = ({ greeting }) => {
  return <h1>{greeting}, world!</h1>;
}
Enter fullscreen mode Exit fullscreen mode

What did we do here?

We defined an interface MyComponentProps to describe our component’s props, ensuring type safety.

By saying greeting is a string, TypeScript will shout at us if we try to pass, say, a number instead.

We also used the FC type (short for Functional Component) from React’s type definitions, making sure TypeScript knows it's a React component.

3. Strongly Typing useState and useEffect

Let's upgrade our components further by adding types to the states and effects, common features of functional components.

Here’s a component with state and an effect:

// Before: Counter.js
import React, { useState, useEffect } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Let's sprinkle some TypeScript magic on this:

// After: Counter.tsx
import React, { useState, useEffect, FC } from 'react';

const Counter: FC = () => {
  // Declare the 'count' state variable with TypeScript
  const [count, setCount] = useState<number>(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>
        Click me
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In our refactored component, we explicitly told TypeScript to expect a number for our count state.

This detail prevents pesky bugs where we might accidentally end up with a string, object, or heaven forbid, null instead of our expected number.

And there we go!

We've successfully refactored our React components to use TypeScript.

By explicitly typing our components, states, and props, we're creating a more predictable and easy-to-maintain codebase.

We're not just coding; we're crafting a masterpiece with the precision it deserves.

Next up, we'll dig deeper into more complex scenarios and how TypeScript comes to our rescue!

Back to Table of Contents


State Management and Context API

Now, let's get into the nitty-gritty of state management in React with TypeScript. If you've used the Context API in a JavaScript project, you know it's a powerful feature for passing data through the component tree without having to manually pass props down at every level. In TypeScript, we get the added benefit of strict typing, which makes our context data even more robust and predictable. Ready to jump in? Let's go!

1. Creating a Typed Context

First, we're going to create a new context with TypeScript. This context will ensure that any default value, provider value, or consumer component matches our expected type.

Here's how you'd define a basic context in JavaScript:

// Before: DataContext.js
import React, { createContext, useState } from 'react';

export const DataContext = createContext();

export const DataProvider = ({ children }) => {
  const [data, setData] = useState(null);

  return (
    <DataContext.Provider value={{ data, setData }}>
      {children}
    </DataContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Now, let's type this context using TypeScript:

// After: DataContext.tsx
import React, { createContext, useState, FC, ReactNode } from 'react';

// First, we define a type for our context's state
interface DataContextState {
  data: any; // Tip: Replace 'any' with the expected type of 'data'
  setData: (data: any) => void; // And here too, replace 'any' with the actual expected type
}

// We ensure our createContext() call is typed with the above interface
export const DataContext = createContext<DataContextState | undefined>(undefined);

// Now, let's create a provider component
export const DataProvider: FC<{children: ReactNode}> = ({ children }) => {
  const [data, setData] = useState<any>(null); // Again, consider replacing 'any' with your actual data type

  // The context provider now expects a value that matches 'DataContextState'
  return (
    <DataContext.Provider value={{ data, setData }}>
      {children}
    </DataContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

What we've done here is create a TypeScript interface, DataContextState, which strictly types our context data.

We've also typed the createContext function and the DataProvider component, ensuring that everything from the state variables to the context values aligns with our defined types.

2. Using the Typed Context

Now that we have our typed DataContext, let's see how we can utilize it within a component. We'll need to use the useContext hook, and here's how that's done:

// ComponentUsingContext.tsx
import React, { useContext, FC } from 'react';
import { DataContext } from './DataContext';

const ComponentUsingContext: FC = () => {
  // Here we're telling TypeScript to expect 'DataContextState' from our context
  const { data, setData } = useContext(DataContext) ?? {};

  // This function would update the context state, triggering re-renders in consuming components
  const handleUpdateData = () => {
    const newData = { message: "Hello, TypeScript!" }; // This should match the structure of your data type
    setData(newData);
  };

  return (
    <div>
      <pre>{JSON.stringify(data, null, 2)}</pre>
      <button onClick={handleUpdateData}>Update Data</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

In ComponentUsingContext, we're accessing the context and expecting TypeScript to validate that the value aligns with DataContextState. Our handleUpdateData function demonstrates how you might update shared state—any components consuming DataContext would re-render with the new data when setData is called.

By using TypeScript with the Context API, we gain confidence that our shared state management is consistent across the application. The compiler catches any discrepancies between what our context provides and what our components expect. This synergy makes our code more reliable and our development process smoother, allowing us to avoid entire categories of bugs that we might otherwise encounter.

Keep up the good work, and remember, a little typing now saves a lot of debugging later!

Back to Table of Contents


Routing and Async Operations

We're making great progress!

Having covered how TypeScript enhances our components and state management, it's time to see it in action in other crucial areas.

Now, we'll explore how TypeScript interplays with routing and asynchronous operations in a React application.

These operations are central to most applications today, and having type safety ensures reliability and predictability in behavior, which is exactly what we aim for.

1. Strongly-Typed Routing with react-router-dom

Routing is integral to any application with multiple views. The react-router-dom library is a staple in the React ecosystem for this purpose. Let's see how we can leverage TypeScript for better routing experiences.

Firstly, if you haven't already, install the types for react-router-dom:

npm install @types/react-router-dom
Enter fullscreen mode Exit fullscreen mode

Below is an example of how you might set up your routes in JavaScript:

// App.js
import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import Home from './Home';
import Profile from './Profile';

function App() {
  return (
    <Router>
      <Switch>
        <Route path="/" exact component={Home} />
        <Route path="/profile" component={Profile} />
      </Switch>
    </Router>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Now, let's refactor this with TypeScript in mind:

// App.tsx
import React, { FC } from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import Home from './Home';
import Profile from './Profile';

// Define types for your route parameters if any
type TParams = { userId: string };

const App: FC = () => {
  return (
    <Router>
      <Switch>
        <Route path="/" exact component={Home} />
        // Use 'render' prop for passing in params with type safety
        <Route path="/profile/:userId" render={({match}) => <Profile userId={match.params.userId as keyof TParams} />} />
      </Switch>
    </Router>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

In the refactored example, we've added types to our route parameters, ensuring that any components depending on these parameters receive them as expected. This approach is particularly useful for feature-rich applications where components are heavily reliant on route parameters.

2. Async Operations with TypeScript

Asynchronous operations are a bedrock of modern web applications. Whether you're fetching data from an API, handling file uploads, or waiting for complex computations, you're dealing with async operations. TypeScript provides a way to handle these operations more predictably by ensuring the data you work with is strictly typed.

Here's a simple example of how you might fetch user data in a React component with JavaScript:

// UserProfile.js
import React, { useEffect, useState } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // Fetch user data when the component mounts
    async function fetchUserData() {
      const response = await fetch(`/api/users/${userId}`);
      const userData = await response.json();
      setUser(userData);
    }

    fetchUserData();
  }, [userId]);

  // Render your user data
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Now, let’s transition this to be type-safe with TypeScript:

// UserProfile.tsx
import React, { useEffect, useState, FC } from 'react';

// Define a type for our user data
interface User {
  id: string;
  name: string;
  email: string;
  // ...
}

interface UserProfileProps {
  userId: string;
}

const UserProfile: FC<UserProfileProps> = ({ userId }) => {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    // Fetch user data when the component mounts
    async function fetchUserData() {
      const response = await fetch(`/api/users/${userId}`);
      if (response.ok) {
        const userData: User = await response.json(); // Here we're asserting the returned data is of type 'User'
        setUser(userData);
      }
    }

    fetchUserData();
  }, [userId]);

  // Render your user data
  // ...
}

export default UserProfile;
Enter fullscreen mode Exit fullscreen mode

By defining a User interface, we're creating an expectation for the shape of the data we plan to receive, ensuring that any interaction with the user state within our component aligns with the defined type. This predictability can significantly simplify debugging and testing, especially in applications with complex data structures and heavy API interactions.

With these enhancements, not only is our code more reliable due to the compiler's type checking, but we also provide fellow developers (and our future selves) a clearer picture of the data structures and parameters we're working with. This clarity makes maintaining and expanding our applications much more straightforward.

Keep rocking, and remember, TypeScript is more than a tool; it's a safety net ready to catch you when the unexpected happens!

Back to Table of Contents


Testing in TypeScript

Now that we've seen how TypeScript improves various aspects of our React application, it's time to talk about another critical area: testing.

Testing is fundamental in ensuring our app works as expected, and TypeScript can make our tests more reliable and efficient.

Let's dig into how TypeScript plays a role in testing, particularly in a React project.

1. Setting the Stage for Testing

Before we get into the code, make sure you have the necessary libraries installed for testing in a React project. Here's a quick setup with Jest and React Testing Library, widely used together for testing React applications:

npm install --save-dev jest @types/jest @testing-library/react @testing-library/jest-dom
Enter fullscreen mode Exit fullscreen mode

These libraries provide a robust environment for writing unit and integration tests. Now, let's consider a real-world scenario for clarity.

2. Real-World Testing Scenario: User Greeting Component

Imagine we have a simple component in our app that greets users based on the time of day. It's a functional component that takes the user's name as a prop and the current time as a state.

Here's what our UserGreeting component might look like:

// UserGreeting.tsx
import React, { FC, useState, useEffect } from 'react';

interface UserGreetingProps {
  name: string;
}

const UserGreeting: FC<UserGreetingProps> = ({ name }) => {
  const [currentHour, setCurrentHour] = useState(new Date().getHours());
  const [greeting, setGreeting] = useState('');

  useEffect(() => {
    // Determine the time of day and set the appropriate greeting
    if (currentHour < 12) {
      setGreeting('Good morning');
    } else if (currentHour < 18) {
      setGreeting('Good afternoon');
    } else {
      setGreeting('Good evening');
    }
  }, [currentHour]);

  return (
    <div>
      <h1>{greeting}, {name}!</h1>
    </div>
  );
}

export default UserGreeting;
Enter fullscreen mode Exit fullscreen mode

Now, we need to write tests to ensure our component behaves as expected under different conditions. Our test cases will confirm that the appropriate greeting is displayed based on the time of day.

Here's how we can write these tests using Jest and React Testing Library:

// UserGreeting.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import UserGreeting from './UserGreeting';

describe('UserGreeting Component', () => {
  // Mock date for consistent testing
  const originalDate = Date;

  beforeAll(() => {
    const mockDate = new Date(2023, 10, 17, 14); // 2:00 PM
    global.Date = jest.fn(() => mockDate) as any;
  });

  afterAll(() => {
    global.Date = originalDate; // Restore original Date object
  });

  it('displays the correct greeting for the afternoon', () => {
    render(<UserGreeting name="Jordan" />);

    // Assert the greeting based on the mocked time of day
    expect(screen.getByText('Good afternoon, Jordan!')).toBeInTheDocument();
  });

  // Additional tests would repeat this process for other times of day,
  // ensuring our component behaves consistently.
});

Enter fullscreen mode Exit fullscreen mode

In this script, we're rendering our component with a set time (mocked to be 2:00 PM) and checking if it outputs "Good afternoon" as expected. We can write more tests for other times of day (morning, evening) to ensure our component is fully covered.

Through TypeScript, we make sure that the props we pass to our components in our tests match the types expected. This way, we avoid running into issues with incorrect props that could lead to false negatives in our tests, ensuring that our tests are robust and reliable.

Using TypeScript in testing helps catch issues early in the development process, making our apps more robust and maintainable. It's a win-win situation!

Remember, consistent and comprehensive testing is a hallmark of high-quality software development. Keep it up!

Back to Table of Contents


Handling Non-TypeScript Packages

Alright, let's tackle an area that often trips up folks when shifting over to TypeScript in a React project: dealing with JavaScript libraries and packages that aren't written in TypeScript. It's a common scenario; you've got your TypeScript project up and running, and then you install a third-party package, only to find your TypeScript compiler complaining. Don't worry; there are solutions.

1. Encountering the Problem

Here's a typical scenario: you're trying to use a package that doesn't have TypeScript support out of the box, and the TypeScript compiler starts throwing errors like "Could not find a declaration file for module 'module-name'." Sound familiar?

This issue arises because TypeScript relies on type definitions to understand the structure of libraries and packages. If these type definitions are missing, TypeScript gets a bit lost. But fear not, we've got strategies to handle this.

2. Using DefinitelyTyped

One of the first things you can do is check if the community has provided type definitions for the package via DefinitelyTyped. DefinitelyTyped is a massive repository of type definitions maintained by the community.

Here's how you would check and use types from DefinitelyTyped:

  1. Search for type definitions for your package by trying to install them using npm. Type definitions on DefinitelyTyped are usually prefixed with @types/.
npm install @types/package-name
Enter fullscreen mode Exit fullscreen mode

For instance, if you were using the lodash library, you would run:

npm install @types/lodash
Enter fullscreen mode Exit fullscreen mode
  1. After installing, you don't need to import the types anywhere in your project explicitly. TypeScript will automatically detect and use them, allowing you to import and use libraries as usual, and get autocompletion and type-checking.

But what if there's no type definition available on DefinitelyTyped?

3. Crafting Your Own Declaration File

If DefinitelyTyped doesn't have the type definitions you need, it's time to create a custom declaration file. While this approach requires more effort, it ensures your TypeScript project works smoothly with the JavaScript library.

Here's a simplified version of what you might do:

  1. Create a new file with a .d.ts extension within your project's source (or types) directory. This could be something like declarations.d.ts.

  2. In this file, you'll want to declare the module and potentially outline the basic structure you expect from the library. For instance:

// This is a simplistic type declaration file for a hypothetical package.

// We declare the module so TypeScript recognizes it.
declare module 'name-of-untyped-package' {

  // Below, we're declaring a very basic structure. It's saying
  // there's a function we're expecting to exist, which returns any.
  // Ideally, you'd want to flesh this out with more specific types
  // if you know them or as you learn more about the library.
  export function functionName(arg: any): any;

  // You can continue to define the shapes of other functions or variables
  // you expect to exist within the package. The more detailed you are here,
  // the more helpful your type checking will be.
}
Enter fullscreen mode Exit fullscreen mode

This homemade declaration file won't be as comprehensive as a full set of type definitions, but it tells TypeScript, "Trust me, I know this module exists, and it provides these functions/variables." From here, you can build out more detailed definitions as needed.

Remember, dealing with non-TypeScript packages can be a bit of a hurdle, but these strategies ensure your TypeScript project remains robust and enjoys the type safety and predictability we're after. It's all about that confidence in your codebase!

Back to Table of Contents


Best Practices and Common Pitfalls

Switching to TypeScript in your React project isn't just about changing file extensions and adding type annotations. It's also about adapting your mindset and development practices to make the most of what TypeScript offers while avoiding common stumbling blocks. So, let's discuss some best practices and common pitfalls you might encounter during this journey.

1. Best Practices

1.1 Lean on Type Inference

While it might be tempting to annotate everything, one of TypeScript's strengths is its type inference. It's often unnecessary to add explicit types to every piece of your code.

// Instead of this:
let x: number = 0;

// You can rely on type inference:
let x = 0;  // TypeScript knows this is a number
Enter fullscreen mode Exit fullscreen mode

Over-annotating can make your code verbose without adding value. Trust TypeScript to infer types where it can.

1.2 Embrace Utility Types

Utility types provide flexible ways to handle types in various scenarios. They can save you a lot of effort and make your type handling more efficient.

// Example of using Partial to make all properties in an object optional
function updateProfile(data: Partial<UserProfile>) {
  // function implementation
}

// Now you can pass only the parts of UserProfile you need to update
updateProfile({ username: "newUserName" });  // This is valid
Enter fullscreen mode Exit fullscreen mode

Partial, Readonly, Pick, and other utility types can be incredibly handy.

1.3 Use Enums for Known Sets of Constants

When you have a property that can only take specific values, using enum can make your intent clear while providing validation on those values.

enum UserRole {
  Admin = 'ADMIN',
  User = 'USER',
  Guest = 'GUEST',
}

// Now UserRole can only be one of the values defined in the enum
function assignRole(role: UserRole) {
  // function implementation
}
Enter fullscreen mode Exit fullscreen mode

1.4 Prefer Interfaces for Object Structure Definition

While type and interface can often be used interchangeably, using interface for defining the structure of objects or classes makes your code more readable and provides better error messages.

interface UserProfile {
  username: string;
  email: string;
  // More properties...
}
Enter fullscreen mode Exit fullscreen mode

2. Common Pitfalls

2.1 Overusing any

Using any negates the benefits of TypeScript by bypassing type checking. While it might seem like a quick fix, it makes your code less safe and predictable.

// Try to avoid this:
let userData: any = fetchData();

// Instead, define a type for the data you expect:
let userData: UserProfile = fetchData();
Enter fullscreen mode Exit fullscreen mode

2.2 Ignoring Compiler Warnings

TypeScript's compiler warnings are there to help you. Ignoring these can lead to the same kinds of bugs and issues you're trying to avoid by using TypeScript.

2.3 Getting Lost in Complex Types

Sometimes, in an attempt to make types precise, developers create incredibly complex type definitions that are hard to understand and maintain. If your types are getting convoluted, it might be time to simplify or refactor your code.

2.4 Forgetting Third-Party Library Types

If you're using third-party libraries, always check if there are existing TypeScript types on DefinitelyTyped. Not doing so can mean missing out on type safety features for these libraries.

In conclusion, adopting TypeScript is more than just using a new syntax; it's about adopting new practices that help avoid errors, make code more readable, and improve maintenance. Avoid common traps, and remember, the goal is to write cleaner, more reliable, and more maintainable code!

Back to Table of Contents


Conclusion

Well, folks, we've reached the end of our TypeScript migration journey. It's been quite a ride, hasn't it? We started with the big question of "why" and delved into the nitty-gritty of actually shifting a React project from JavaScript to TypeScript. From setting up your TypeScript environment to refactoring components, managing states, handling routes, and even dealing with those pesky non-TypeScript packages, we've covered a lot of ground.

Reflecting on this journey, it's clear that migrating to TypeScript isn't a mere 'search-and-replace' of .js files with .tsx. It's a strategic move that involves learning new conventions, understanding types deeply, and, most importantly, changing the way we think about our code's reliability and consistency.

Here are a few takeaways as we wrap up:

  1. Safety Net: TypeScript has introduced a safety layer to our project, catching errors before they wreak havoc at runtime. This safety net, once you get used to it, is a game-changer in terms of confidence in your code and overall development speed.

  2. Clearer Communication: With types, our code now communicates more explicitly. Whether it's you revisiting your code or a new team member trying to understand your component structures, TypeScript serves as an additional documentation layer.

  3. Refactoring Confidence: Scared of refactoring? Well, TypeScript has your back. With types ensuring contracts within your code, many potential errors are caught during refactoring phases, making the process less daunting.

  4. Community and Ecosystem: Embracing TypeScript opens doors to a thriving ecosystem. From typed libraries on DefinitelyTyped to endless support on community forums and more streamlined third-party package integration, you're in good company.

  5. Learning Curve: Yes, TypeScript introduces a learning curve. There were probably moments of frustration, confusions around types and interfaces, or wrestling with the compiler. But, look back at your journey and you'll see how much more you understand your code and its behavior now.

Remember, the transition to TypeScript is not a sprint; it's a marathon. There might be a few hurdles initially, but the long-term gains in code quality, predictability, and maintainability are well worth the effort.

As you continue your development journey, keep exploring, learning, and sharing your experiences with TypeScript. Every challenge is an opportunity to learn. Your future self (and your team) will thank you for the robust, type-safe, and significantly more maintainable codebase you're cultivating today.

Thank you for joining me in this exploration of TypeScript with React. Keep coding, keep improving, and, most importantly, enjoy the process!

Stay Connected

If you enjoyed this article and want to explore more about web development, feel free to connect with me on various platforms:

dev.to

hackernoon.com

hashnode.com

twitter.com

Your feedback and questions are always welcome.
Keep learning, coding, and creating amazing web applications.

Happy coding!

Back to Table of Contents


Additional Resources

Even though our guide has come to an end, your adventure with TypeScript doesn't stop here. The world of TypeScript is vast, with a plethora of resources to explore, learn from, and contribute to. Below are some valuable resources that can help reinforce your understanding and keep you updated in the TypeScript community.

  1. TypeScript Official Documentation: There's no better place to explore TypeScript than its official website. It's packed with detailed documentation, examples, and explanations on various features.

  2. DefinitelyTyped: When working with third-party libraries, DefinitelyTyped is a lifesaver. It’s a massive repository of high-quality TypeScript type definitions.

  3. React TypeScript Cheatsheet: This comprehensive cheatsheet caters specifically to React developers transitioning to TypeScript, covering common patterns and practices.

  4. TypeScript Deep Dive: An excellent online book that offers a detailed exploration of TypeScript. Deep Dive explains the nitty-gritty of TypeScript with a focus on practical scenarios.

  5. TypeScript GitHub Repository: Engage with the community and stay up-to-date with the latest developments in TypeScript by visiting the official TypeScript GitHub repository.

  6. Stack Overflow: The TypeScript tag on Stack Overflow is a hub of common (and uncommon) queries and nuanced use-cases encountered by developers worldwide. It's a gold mine of practical insights.

  7. TypeScript Weekly: A curated newsletter, TypeScript Weekly delivers the latest articles, tips, and resources straight to your inbox.

  8. Reddit and Discord Channels: Communities on platforms like Reddit’s r/typescript and various Discord channels host vibrant discussions, news, and problem-solving threads related to TypeScript.

  9. Official TypeScript Blog: For announcements, deep dives, and tutorials from the TypeScript team, check out the official blog.

  10. Online Coding Platforms: Interactive learning experiences through platforms like Codecademy, freeCodeCamp, and Scrimba provide hands-on TypeScript courses.

Remember, communities thrive on participation. Don't hesitate to ask questions, contribute answers, or share your solutions and experiences. The collective wisdom of community forums, official documentation, and continuous practice will guide you towards TypeScript mastery. Happy coding!

Back to Table of Contents

Top comments (0)