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.
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.js → index.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 configurationtsconfig.json— TypeScript compiler settingssrc/— source directorysrc/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.tsxfiles.lib— declares which library files to include; specifyinges2015permits ECMAScript 6 syntax.module— defines module code generation.noImplicitAny— raises errors for types that are implicitlyany.outDir— sets the output directory.sourceMap— emits a.mapfile 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 namedbundle.js.resolve— dictates which file extensions webpack considers, here.js,.jsx,.json,.ts, and.tsx.module.rules— assigns loaders:awesome-typescript-loaderfor.tsx/.tsfiles,source-map-loaderfor.jsfiles, andcss-loaderfor.cssfiles.plugins— extends webpack's base abilities.html-webpack-pluginbuilds 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:
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:
- Rename the file's extension to
.tsx. - 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…":
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.tsxand an action module inaction.tsfor fetching episodes. - Make an
EpisodeList.tsxcomponent to display fetched episodes, importing it with React Lazy and Suspense. - Extend the store and actions to support adding episodes, also from
store.tsxandaction.ts. - Add removal: wire deletion targets in the store and action files.
- For favorites, import
EpisodeListinto 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.
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— holdsepisodesandfavorites, both typed asArray<IEpisode>.IAction— definespayloadasArray | anyandtypeasstring.
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:
createContextanduseReducercome from React.- The
initialStateobject is typed asIStatewith empty arrays forepisodesandfavorites. - The
Storevariable holds the context, typed as<IState | any>. - A
reducerfunction acceptsstate: IStateandaction: IAction. For aFETCH_DATAaction type, it returns a new object spreading the current state and settingepisodesto the action payload; adefaultcase returns the unmodified state. - The exported
StoreProvidercomponent passeschildrenas a prop, callsuseReducer, and provides bothstateanddispatchthrough 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-boxclass with a uniquekeyfromepisode.id. - An
imgtag using a ternary to displayepisode.image.mediumif present, or an empty string. - A div for
episode.name. - A section showing the season and episode number, plus a
Favbutton.
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 } = storereturn 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:
- “How To Migrate A React App To TypeScript” – a step-by-step guide by Joe Previte.
- “Why And How To Use TypeScript In Your React App?” – a deep dive by Mahesh Haldar.
If you plan to continue expanding your tooling knowledge, these related articles on Smashing Magazine offer useful context on modern front-end workflows:
- A Guide To Redux Toolkit With TypeScript
- The Key To Good Component Design Is Selfishness
- Statoscope: A Course Of Intensive Therapy For Your Bundle
- New CSS Viewport Units Do Not Solve The Classic Scrollbar Problem




