TypeScript’s Role in JavaScript Projects

JavaScript’s dynamic typing means variable types are only resolved at runtime. In large codebases, this can lead to subtle bugs when a variable is reassigned to a different type without warning. TypeScript addresses this by adding a static type system on top of JavaScript. It’s a typed superset of the language, developed by Microsoft, that compiles down to plain JavaScript. Any valid JavaScript program is also valid TypeScript, which makes adoption incremental.

The static type checking that TypeScript provides happens at compile time, not runtime. This catches whole classes of errors before your code ever runs. It also makes large-scale refactoring considerably safer: rather than relying on regex-based search-and-replace across files, an IDE can use commands like “Rename symbol” to update every reference, with TypeScript flagging any type mismatches that result from the change.

That said, TypeScript is not a cure-all. Its type annotations can create a false sense of security; the compiler only verifies what you’ve explicitly typed, and static typing does not by itself reduce overall bug density. The typing system can also become complicated precisely because it must interoperate fully with JavaScript. For teams looking to reduce bugs, test-driven development remains a complementary practice. TypeScript is most valuable when you expect to maintain an application over a long period — its self-documenting nature helps new developers onboard — or when you’re building a library and want to ship accurate type suggestions to its users.

Approach 1: Create React App

Since version 2.1, Create React App ships with built-in support for TypeScript, so bootstrapping a new project requires little extra setup.

Announcement of TypeScript in Create React App (Large preview)

To scaffold a project with the TypeScript template, run either of these commands:

npx create-react-app my-app --folder-name

or

yarn create react-app my-app --folder-name

For an existing Create React App project, you need to install TypeScript along with the type definitions for React and ReactDOM.

npm install --save typescript @types/node @types/react @types/react-dom @types/jest

or

yarn add typescript @types/node @types/react @types/react-dom @types/jest

After installation, rename your entry files from .js to .tsx (for example, index.jsindex.tsx) and restart the development server. That’s the entire migration for this path.

Bundling React, TypeScript, and Webpack

Webpack acts as a static module bundler, taking your application code—JavaScript, node_modules packages, images, and CSS—and packaging it into reusable chunks that work in the browser. For a TypeScript-based React project, this requires some initial configuration.

Project Scaffolding And Dependencies

Start fresh in a new directory and initialize npm:

mkdir react-webpack
cd react-webpack
npm init -y

This creates a package.json with defaults. Then install the tooling you'll need—webpack, TypeScript, React-specific loaders, and related modules:

#Installing devDependencies

npm install --save-dev @types/react @types/react-dom awesome-typescript-loader css-loader html-webpack-plugin mini-css-extract-plugin source-map-loader typescript webpack webpack-cli webpack-dev-server

#installing Dependencies
npm install react react-dom

After installation, manually create the following project structure:

  • webpack.config.js — webpack configuration
  • tsconfig.json — TypeScript compiler settings
  • src/ — source directory
  • src/components/ — component directory
  • Inside components/: index.html, App.tsx, index.tsx

The resulting layout resembles:

├── package.json
├── package-lock.json
├── tsconfig.json
├── webpack.config.js
├── .gitignore
└── src
    └──components
        ├── App.tsx
        ├── index.tsx
        ├── index.html

Core Source Files

The index.html file provides a minimal HTML shell with an empty div carrying the ID output:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>React-Webpack Setup</title>
</head>
<body>
  <div id="output"></div>
</body>
</html>

The App.tsx component defines an interface, HelloWorldProps, with userName and lang as typed string properties. This interface is passed as props to the exported App component:

import * as React from "react";
export interface HelloWorldProps {
  userName: string;
  lang: string;
}
export const App = (props: HelloWorldProps) => (
  <h1>
    Hi {props.userName} from React! Welcome to {props.lang}!
  </h1>
);

Finally, index.tsx imports that App component. When webpack encounters .ts or .tsx extensions, it delegates transpilation to the awesome-typescript-loader library:

import * as React from "react";
import * as ReactDOM from "react-dom";
import { App } from "./App";
ReactDOM.render(
  <App userName="Beveloper" lang="TypeScript" />,
  document.getElementById("output")
);

TypeScript Configuration

The tsconfig.json file holds all compiler options. Key settings include:

  • compilerOptions — the root block of compiler settings.
  • jsx:react — enables JSX syntax within .tsx files.
  • lib — declares which library files to include; specifying es2015 permits ECMAScript 6 syntax.
  • module — defines module code generation.
  • noImplicitAny — raises errors for types that are implicitly any.
  • outDir — sets the output directory.
  • sourceMap — emits a .map file for easier debugging.
  • target — the ECMAScript version to transpile down to, chosen by browser requirements.
  • include — lists files to include in the compilation.

A practical configuration is shown here:

{
  "compilerOptions": {
    "jsx": "react",
    "module": "commonjs",
    "noImplicitAny": true,
    "outDir": "./build/",
    "preserveConstEnums": true,
    "removeComments": true,
    "sourceMap": true,
    "target": "es5"
  },
  "include": [
    "src/components/index.tsx"
  ]
}

Webpack Configuration And Scripts

Webpack's behavior is driven from webpack.config.js. Its meaningful properties:

  • entry — the starting file(s) for the build.
  • output — where the bundled result goes, typically named bundle.js.
  • resolve — dictates which file extensions webpack considers, here .js, .jsx, .json, .ts, and .tsx.
  • module.rules — assigns loaders: awesome-typescript-loader for .tsx/.ts files, source-map-loader for .js files, and css-loader for .css files.
  • plugins — extends webpack's base abilities. html-webpack-plugin builds a template served to the browser from .src/component/index.html.

MiniCssExtractPlugin extracts and manages the parent CSS file for the application.

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  entry: "./src/components/index.tsx",
  target: "web",
  mode: "development",
  output: {
    path: path.resolve(\__dirname, "build"),
    filename: "bundle.js",
  },
  resolve: {
    extensions: [".js", ".jsx", ".json", ".ts", ".tsx"],
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        loader: "awesome-typescript-loader",
      },
      {
        enforce: "pre",
        test: /\.js$/,
        loader: "source-map-loader",
      },
      {
        test: /\.css$/,
        loader: "css-loader",
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(\__dirname, "src", "components", "index.html"),
    }),
    new MiniCssExtractPlugin({
      filename: "./src/yourfile.css",
    }),
  ],
};

Adding scripts to package.json lets you build and run the app easily:

"scripts": {
"start": "webpack-dev-server --open",
"build": "webpack"
},

Running npm start confirms the setup worked:

React-Webpack setup output (Large preview)

For deeper exploration, the full configuration is available in a companion repository.

Migrating An Existing Create React App To TypeScript

Adopting TypeScript incrementally in an active React project is often preferable to a full rewrite, which would trigger large, error-prone pull requests. This step-by-step path keeps changes manageable.

Add TypeScript And A Configuration File

First, install TypeScript and its type definitions. With a Create React App project, this command handles setup without affecting the current build—running npm start still behaves the same:

# Using npm
npm install --save typescript @types/node @types/react @types/react-dom @types/jest

# Using Yarn
yarn add typescript @types/node @types/react @types/react-dom @types/jest

Next, generate a tsconfig.json using the TypeScript CLI, which scaffolds starters with comments:

npx tsc --init

Replace that scaffolded content with the focused configuration below:

{
    "compilerOptions": {
      "jsx": "react",
      "module": "commonjs",
      "noImplicitAny": true,
      "outDir": "./build/",
      "preserveConstEnums": true,
      "removeComments": true,
      "sourceMap": true,
      "target": "es5"
    },
    "include": [
      "./src/**/**/\*"
    ]
  }

Compiler options vary by project needs—the TypeScript options reference is the authority here. Common settings include target for transpiling down to older ECMAScript versions, lib for adding library files, jsx:react for JSX support, module for module code, noImplicitAny to flag implicit any types, outDir for output paths, sourceMap for debug maps, and include to filter the file list.

Convert Components In Small Batches

TypeScript's gradual adoption model means you can convert files one at a time. Pick a small component that uses clearly defined properties to start.

For each component, two changes are required:

  1. Rename the file's extension to .tsx.
  2. Add explicit type annotations using TypeScript's React patterns.

Bulk Renaming Files on macOS

Individually renaming files gets tedious in a larger codebase. On a Mac, locate the folder in Finder by right-clicking it and choosing "Reveal in Finder." Select the files to rename, right-click them, and pick "Rename X items…":

Rename files on a Mac (Large preview)

Enter the search and replacement strings, click "Rename," and the files convert in one pass.

Bulk Renaming Files on Windows

Windows offers similar bulk renaming options, but the steps go beyond this scope—a complete guide is available for that workflow. After renaming, expect type errors to appear; resolving them means adding types wherever inferred types fall short.

Building An Episode-Picker App

To see TypeScript in action, build an episode-picker for the Money Heist series. This assumes comfort with TypeScript's basic types.

Breaking the build into several chunks keeps it approachable:

  • Scaffold a Create React App project.
  • Set up interfaces and types for episodes in interface.ts.
  • Create a store in store.tsx and an action module in action.ts for fetching episodes.
  • Make an EpisodeList.tsx component to display fetched episodes, importing it with React Lazy and Suspense.
  • Extend the store and actions to support adding episodes, also from store.tsx and action.ts.
  • Add removal: wire deletion targets in the store and action files.
  • For favorites, import EpisodeList into the favorites view, then render it there.
  • Add Reach Router for page navigation.

Bootstrapping the React Application

The quickest path to a modern React setup is Create React App, the officially supported tool for building single-page applications with zero configuration. Use it to scaffold the project from your CLI:

npx create-react-app react-ts-app && cd react-ts-app

After installation completes, launch the development server with npm start.

React starter page (Large preview)

Defining Contracts: Types and Interfaces

TypeScript interfaces define the shape of objects, making them the right tool for typing the data structures in this project. When a mismatch occurs—say, assigning a string to a property typed as number—the compiler flags it immediately. A file like interface.ts centralizes these definitions:

/**
|--------------------------------------------------
| All the interfaces!
|--------------------------------------------------
*/
export interface IEpisode {
  airdate: string
  airstamp: string
  airtime: string
  id: number
  image: { medium: string; original: string }
  name: string
  number: number
  runtime: number
  season: number
  summary: string
  url: string
}
export interface IState {
  episodes: Array<IEpisode>
  favourites: Array<IEpisode>
}
export interface IAction {
  type: string
  payload: Array<IEpisode> | any
}
export type Dispatch = React.Dispatch<IAction>
export type FavAction = (
  state: IState,
  dispatch: Dispatch,
  episode: IEpisode
) => IAction

export interface IEpisodeProps {
  episodes: Array<IEpisode>
  store: { state: IState; dispatch: Dispatch }
  toggleFavAction: FavAction
  favourites: Array<IEpisode>
}
export interface IProps {
  episodes: Array<IEpisode>
  store: { state: IState; dispatch: Dispatch }
  toggleFavAction: FavAction
  favourites: Array<IEpisode>
}

Prefixing interface names with “I” is a common convention that improves readability, though it’s optional.

This project relies on three core interfaces:

  • IEpisode — mirrors the API response fields (airdate, airstamp, airtime, id, image, name, number, runtime, season, summary, url) with matching data types.
  • IState — holds episodes and favorites, both typed as Array<IEpisode>.
  • IAction — defines payload as Array | any and type as string.

The dispatch function type follows the standard React.Dispatch from @types/react, parameterized with <IAction>. Visual Studio Code’s built-in TypeScript checker can suggest these types on hover, reducing guesswork.

Central State Management

Episodes need a central store to hold initial data and a reducer function. Using the useReducer hook, create a store.tsx file:

import React, { useReducer, createContext } from 'react'
import { IState, IAction } from './types/interfaces'
const initialState: IState = {
  episodes: [],
  favourites: []
}
export const Store = createContext(initialState)
const reducer = (state: IState, action: IAction): IState => {
  switch (action.type) {
    case 'FETCH_DATA':
      return { ...state, episodes: action.payload }
    default:
      return state
  }
}
export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => {
  const [state, dispatch] = useReducer(reducer, initialState)
  return {children}
}

The store pattern works as follows:

  • createContext and useReducer come from React.
  • The initialState object is typed as IState with empty arrays for episodes and favorites.
  • The Store variable holds the context, typed as <IState | any>.
  • A reducer function accepts state: IState and action: IAction. For a FETCH_DATA action type, it returns a new object spreading the current state and setting episodes to the action payload; a default case returns the unmodified state.
  • The exported StoreProvider component passes children as a prop, calls useReducer, and provides both state and dispatch through the context value.

Data Fetching Actions

API requests live in a dedicated Action.ts file:

import { Dispatch } from './interface/interfaces'
export const fetchDataAction = async (dispatch: Dispatch) => {
  const URL =
    'https://api.tvmaze.com/singlesearch/shows?q=la-casa-de-papel&embed=episodes'

  const data = await fetch(URL)
  const dataJSON = await data.json()
  return dispatch({
    type: 'FETCH_DATA',
    payload: dataJSON.\_embedded.episodes
  })
}

The asynchronous fetchDataAction takes dispatch as a parameter. It fetches from a hard-coded API URL, converts the response to JSON, and returns a dispatch call. The action object carries a type of FETCH_DATA and sets the payload to the _embedded.episodes array from the endpoint response.

Reusable Display Component

To keep the app modular, all fetched episodes render through a dedicated component. Create EpisodesList.tsx in the components folder:

import React from 'react'
import { IEpisode, IProps } from '../types/interfaces'
const EpisodesList = (props: IProps): Array<JSX.Element> => {
  const { episodes } = props
  return episodes.map((episode: IEpisode) => {
    return (
      <section key={episode.id} className='episode-box'>
        <img src={!!episode.image ? episode.image.medium : ''} alt={`Money Heist ${episode.name}`} />
        <div>{episode.name}</div>
        <section style={{ display: 'flex', justifyContent: 'space-between' }}>
          <div>
            Season: {episode.season} Number: {episode.number}
          </div>
          <button
            type='button'
          >
            Fav
          </button>
        </section>
      </section>
    )
  })
}
export default EpisodesList

This component receives props typed as IProps and returns an array of JSX elements. Its declared return type, Array<JSX.Element>, is the generic form of JSX.Element[]; the generic syntax appears throughout this project. Inside, episodes comes from destructured props, each mapped to HTML output with:

  • An episode-box class with a unique key from episode.id.
  • An img tag using a ternary to display episode.image.medium if present, or an empty string.
  • A div for episode.name.
  • A section showing the season and episode number, plus a Fav button.

Handling Routing and Initial Load

The home page kickstarts the API call and renders the episode list. Import React hooks (useContext, useEffect), the lazy and Suspense components, plus Store, IEpisodeProps, and FetchDataAction.

The EpisodesList component is imported with React.lazy, enabling code-splitting. This dynamically loads the component rather than bundling it upfront, which trims the initial payload and boosts performance.

The page pulls state and dispatch from the store. A logical AND (&&) check inside useEffect only calls fetchDataAction when the episodes array is empty. A Suspense wrapper shows a loading fallback while data arrives.

Wiring the App Entry Point

Rename index.js to index.tsx. The entry file imports StoreProvider, HomePage, and the stylesheet:

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import { StoreProvider } from './Store'
import HomePage from './components/HomePage'
ReactDOM.render(
  <StoreProvider>
      <HomePage />
  </StoreProvider>,
  document.getElementById('root')
)

Wrapping HomePage inside StoreProvider gives the component and its children access to the shared state.

Replace the default index.css with the project’s own stylesheet to give the interface structure:

html {
  font-size: 14px;
}
body {
  margin: 0;
  padding: 0;
  font-size: 10px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
    "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
.episode-layout {
  display: flex;
  flex-wrap: wrap;
  min-width: 100vh;
}
.episode-box {
  padding: .5rem;
}
.header {
  display: flex;
  justify-content: space-between;
  background: white;
  border-bottom: 1px solid black;
  padding: .5rem;
  position: sticky;
  top: 0;
}

Favorites: Toggle Between Add and Remove

Adding favorites requires extending the store with a new action case. In Store.tsx, the ADD_FAV case returns an object that copies the existing state and appends the new payload to the favorites array:

import React, { useReducer, createContext } from 'react'
import { IState, IAction } from './types/interfaces'
const initialState: IState = {
  episodes: [],
  favourites: []
}
export const Store = createContext<IState | any>(initialState)
const reducer = (state: IState, action: IAction): IState => {
  switch (action.type) {
    case 'FETCH_DATA':
      return { ...state, episodes: action.payload }
    case 'ADD_FAV':
      return { ...state, favourites: [...state.favourites, action.payload] }
    default:
      return state
  }
}
export const StoreProvider = ({ children }: JSX.ElementChildrenAttribute): JSX.Element => {
  const [state, dispatch] = useReducer(reducer, initialState)
  return <Store.Provider value={{ state, dispatch }}>{children}</Store.Provider>
}

An accompanying toggleFavAction in Action.ts accepts dispatch and episodes parameters, typing the state argument as any and the payload as IEpisode | any. It dispatches an object with the ADD_FAV type and the episode as payload.

The EpisodesList component is then expanded. Besides existing props, it now receives toggleFavAction, favorites, and the store. The onClick handler on the button calls toggleFavAction with state, dispatch, and episode. The button label changes based on whether fav.id matches the current episode.id in the favorites list:

import React from 'react'
import { IEpisode, IProps } from '../types/interfaces'
const EpisodesList = (props: IProps): Array<JSX.Element> => {
  const { episodes, toggleFavAction, favourites, store } = props
  const { state, dispatch } = store

  return episodes.map((episode: IEpisode) => {
    return (
      <section key={episode.id} className='episode-box'>
        <img src={!!episode.image ? episode.image.medium : ''} alt={`Money Heist - ${episode.name}`} />
        <div>{episode.name}</div>
        <section style={{ display: 'flex', justifyContent: 'space-between' }}>
          <div>
            Seasion: {episode.season} Number: {episode.number}
          </div>
          <button
            type='button'
            onClick={() => toggleFavAction(state, dispatch, episode)}
          >
            {favourites.find((fav: IEpisode) => fav.id === episode.id)
              ? 'Unfav'
              : 'Fav'}
          </button>
        </section>
      </section>
    )
  })
}
export default EpisodesList

Dedicated Favorites Page

Users need a separate route for their collection. Create FavPage.tsx in the components folder:

import React, { lazy, Suspense } from 'react'
import App from '../App'
import { Store } from '../Store'
import { IEpisodeProps } from '../types/interfaces'
import { toggleFavAction } from '../Actions'
const EpisodesList = lazy<any>(() => import('./EpisodesList'))
export default function FavPage(): JSX.Element {
  const { state, dispatch } = React.useContext(Store)
  const props: IEpisodeProps = {
    episodes: state.favourites,
    store: { state, dispatch },
    toggleFavAction,
    favourites: state.favourites
  }
  return (
    <App>
      <Suspense fallback={<div>loading...</div>}>
        <div className='episode-layout'>
          <EpisodesList {...props} />
        </div>
      </Suspense>
    </App>
  )
}

This component imports lazy, Suspense, the Store, and IEpisodeProps. It uses the same lazy-loading and fallback pattern as the home page, pulling favorited episodes from the store to pass into EpisodesList.

Navigation links require Reach Router. Install the library and its types together:

npm install @reach/router @types/reach__router

The header in App.tsx now imports Link from @reach/router, providing a / path for home and /faves for favorites, with the header displaying the current favorites count via {state.favourites.length}.

Routing is completed in index.tsx, wrapping both FavPage and HomePage in the Router component.

Handling Removal

Removing an episode is essentially the inverse operation. A REMOVE_FAV case is added to the store, returning an object with a copy of the initial state and a favorites array containing the current action payload.

The toggleFavAction function gains the necessary guard. It imports IState to type its state argument. Before dispatching, a variable episodeInFav checks whether the current episode already sits in the favorites list. When it does, a filter removes it:dispatchObj is reassigned with the REMOVE_FAV type and a payload of favWithoutEpisode, so a single button toggles both adding and removing an episode from the collection.

Wrapping Up the TypeScript Setup

By this point, you should have a fully functional React project running on TypeScript, complete with Webpack handling the build pipeline. The migration path from a standard JavaScript React app is straightforward once you have the tsconfig.json and loader configurations in place. The real benefit appears during development: type checking catches whole classes of bugs before your code even reaches the browser, and editor autocompletion becomes noticeably more reliable.

We also walked through a concrete example—building an episode picker app—to show how TypeScript types flow through components, props, and state. In practice, you will find yourself writing interfaces for your data models and props early on, which pays off later when refactoring or adding features, because the compiler immediately flags any mismatched usage.

The complete source code for the example project is available on GitHub. Feel free to clone it, experiment with the configurations, and adapt the patterns to your own projects.

Reference Materials and Further Reading

For those wanting to deepen their understanding, the following resources align closely with the topics we covered:

If you plan to continue expanding your tooling knowledge, these related articles on Smashing Magazine offer useful context on modern front-end workflows:

Smashing Editorial