How to migrate a React app to TypeScript
Migrating a React application from JavaScript to TypeScript can feel like a daunting task, especially for large codebases with complex state management.
As the creator of CoreUI, a widely used open-source UI library, I’ve managed dozens of migrations for enterprise-grade projects with over 25 years of development experience.
The most efficient and modern solution is a gradual migration strategy that leverages the TypeScript compiler’s allowJs option to move files one by one.
This approach ensures your application remains functional throughout the process while you incrementally improve type safety and developer experience.
Install TypeScript dependencies, add a tsconfig.json file, and rename your .js files to .tsx while gradually defining interfaces for props and state.
npm install --save-dev typescript @types/node @types/react @types/react-dom
npx tsc --init
The first step involves adding the core TypeScript engine and the type definitions for React and Node.js. Running npx tsc --init creates a configuration file that allows the compiler to understand how to process your files. By setting allowJs: true in your configuration, you can keep existing JavaScript files working alongside new TypeScript files during the transition.
1. Configure the TypeScript Compiler
Before renaming files, you must configure tsconfig.json to support a hybrid environment. This allows you to migrate at your own pace without breaking the build.
{
"compilerOptions": {
"target": "es2015",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}
This configuration ensures that the TypeScript compiler processes your src folder correctly. The jsx: react-jsx setting is crucial for modern React versions (17+), as it handles the JSX transformation automatically. Setting strict: true is recommended from the start to catch potential bugs early, though you may need to use any temporarily for complex legacy code.
2. Migrating Functional Components
Start by renaming a .js or .jsx file to .tsx and defining an interface for the component props to ensure type safety.
import React from 'react'
interface UserProfileProps {
name: string
age: number
isAdmin?: boolean
}
const UserProfile: React.FC<UserProfileProps> = ({ name, age, isAdmin = false }) => {
return (
<div>
<h1>{name}</h1>
<p>Age: {age}</p>
{isAdmin && <span>Administrator</span>}
</div>
)
}
export default UserProfile
In this example, we define a UserProfileProps interface that specifies exactly what data the component expects. By using React.FC<UserProfileProps>, we tell TypeScript that this is a Functional Component with these specific props. This immediately provides autocompletion and error checking whenever this component is used elsewhere in your application. If you’re new to functional components, see our guide on how to create a functional component in React and how to pass props in React.
3. Typing State and Hooks
React hooks like useState often need explicit types, especially when dealing with objects or arrays that start as null or empty.
import React, { useState, useEffect } from 'react'
interface User {
id: number
username: string
}
const UserList: React.FC = () => {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState<boolean>(true)
useEffect(() => {
// Simulated API call
const timer = setTimeout(() => {
setUser({ id: 1, username: 'coreui_user' })
setLoading(false)
}, 1000)
return () => clearTimeout(timer)
}, [])
if (loading) return <div>Loading...</div>
return <div>{user?.username}</div>
}
The useState<User | null>(null) syntax is a generic that informs TypeScript the state can either hold a User object or be null. This prevents common runtime errors like “cannot read property of null” because TypeScript will force you to check if the user exists before accessing its properties, often via optional chaining (?.). For more on these hooks, check out how to use useState in React and how to use useEffect in React.
4. Handling Form Events and Inputs
Typing events is one of the most common hurdles during migration. React provides specific types for different event categories.
import React, { useState } from 'react'
const SearchForm: React.FC = () => {
const [query, setQuery] = useState('')
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value)
}
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
console.log('Searching for:', query)
}
return (
<form onSubmit={handleSubmit}>
<input
type='text'
value={query}
onChange={handleChange}
/>
<button type='submit'>Search</button>
</form>
)
}
Using React.ChangeEvent<HTMLInputElement> allows TypeScript to know that event.target is an input element, giving you access to the value property without type casting. Similarly, React.FormEvent is used for form submissions. If you are using CoreUI Form Validation, these types ensure your custom validation logic remains robust and bug-free.
5. Integrating with CoreUI Components
When migrating a project that uses UI libraries, ensure you are using the library’s built-in types for a seamless experience.
import React from 'react'
import { CButton, CCard, CCardBody } from '@coreui/react'
const ActionCard: React.FC = () => {
return (
<CCard style={{ width: '18rem' }}>
<CCardBody>
<h5>Card Title</h5>
<p>This component is now fully typed.</p>
<CButton
color='primary'
onClick={() => console.log('Clicked')}
>
Submit
</CButton>
</CCardBody>
</CCard>
)
}
CoreUI components come with built-in TypeScript definitions. When you use components like CButton or CCard, TypeScript automatically validates that you are passing the correct props (like color or onClick). This is the same architecture we use in the React Dashboard Template to provide a high-quality developer experience out of the box.
6. Typing Refs and DOM Elements
Managing DOM references requires specific types to avoid errors when accessing element properties.
import React, { useRef, useEffect } from 'react'
const FocusInput: React.FC = () => {
const inputRef = useRef<HTMLInputElement>(null)
const handleFocus = () => {
// The current property might be null, so we check first
if (inputRef.current) {
inputRef.current.focus()
}
}
return (
<div>
<input ref={inputRef} type='text' />
<button onClick={handleFocus}>Focus the input</button>
</div>
)
}
The useRef<HTMLInputElement>(null) hook specifies that the reference will point to an HTML input element. Because refs are initialized as null and only populated after the component mounts, TypeScript reminds you to perform a null check before calling methods like .focus(). This effectively eliminates “null pointer” exceptions in your UI logic. Learn more in our guide on how to use useRef in React.
7. Working with External Data
When fetching data, creating interfaces for your API responses is the best way to ensure consistency across your application layers.
import React from 'react'
interface ApiResponse {
data: {
items: string[]
total: number
}
}
const DataDisplay: React.FC<{ rawData: ApiResponse }> = ({ rawData }) => {
const itemsCount = rawData.data.items.length
return (
<div>
<p>Total Items: {rawData.data.total}</p>
<p>Current Page Count: {itemsCount}</p>
</div>
)
}
By defining the ApiResponse interface, you ensure that any part of the app consuming this data knows exactly what properties are available. If an API change removes the items array, TypeScript will immediately highlight all broken components during development, rather than failing in production.
Best Practice Note:
We always recommend a “strict” migration for new components while allowing “loose” types for legacy code using the any keyword where necessary. This prevents the migration from stalling.
This is the same approach we use in CoreUI components to ensure reliability while maintaining flexibility for developers.
For a head start on a perfectly configured TypeScript environment, consider using our Free React Admin Template, which comes pre-configured with all the best practices mentioned above.



