As you probably already know, TypeScript is a popular Static Type Checker, built as a superset of JavaScript. TypeScript is really handy for finding type errors before executing them — a rule that is missing from plain Javascript. However, Typescript runs just as smooth as Javascript.
We will assume you started a project in JavaScript and it is too late to start over. So, you have a problem: you want to work in TypeScript, but you are too far along to restart.
What do you do?
In this article, we will cover a way to smoothly and efficiently migrate over to Typescript.
When migrating over to TypeScript, one basic thing to keep in mind is TypeScript files have a .ts
extension instead of .js
. That’s simple enough. Another thing to keep in mind that makes migrating over a breeze is that you can configure your TypeScript compiler to allow JavaScript files.
As we said earlier, TypeScript is just a superset on top of Javascript — therefore, you can migrate one file at a time and not worry about creating an entirely separate branch for the migration.
Speaking of compilers, it would be nice to leave alone whatever currently bundles or compiles your JavaScript. TypeScript has an easy way to simply add itself on top of your compiler without messing things up. If you’re using npm
, you can simply run npm i -g typescript
.
Before getting further into the TypeScript set up, one thing to consider is whether your IDE is configured to work with TypeScript. The pre-compile auto-correct TypeScript offers, as well as the ability to detect errors at this stage, are features you do not want to miss out on. If you are using VS Code, support for TypeScript is built in. Otherwise, you will likely need to find a package to get your IDE support for TypeScript.
Now you’ll want to add the TypeScript compiler.
Create a file in the root directory of your project named tsconfig.json
. There are hundreds of ways to configure this file, but let’s just look at some of the basics.
Before we go into it a bit, instead of creating the file yourself, you can run this command in the root directory of you project:
npx tsc --init
This will create the tsconfig.json
file for you, with a few default options turned on, along with a bunch of possible options commented out for you to see.
As you can see, this will be a JSON file. As such, the two common properties to configure here are compileOptions
and include
.
CompileOptions
is an object where options can be set to true to change the manner in which TypeScript transpiles files into JavaScript.
For instance, in compileOptions
, we can turn on noImplicitAny: true
(if false, TypeScript will infer the type of “any” where no types are defined) and strictNullChecks: true
(if false, Typescript will ignore “null” and “undefined”).
With these two options turned on, we can be sure our .ts
files will actually check for types. If not, you could run plain JavaScript in .ts
files.
The include
option is a way to specify the filenames or the glob patterns to match TypeScript files. It is an array which can include specific file names or a pattern all your TypeScript files follow. A simple set up is to simply define this option as include: ["src/**/*"]
(*_
matches any directory, while _
matches any file).
This would default to pick any file under the src
directory with a .ts
, .tsx
, or .d.ts
extension.
If you add allowJs: true
to the compileOptions
, the include
defined above would also include .js
and .jsx
files from our src
directory.
Short side note: the difference between .d.ts
extension and plain .ts
is that .d.ts
is for declaring things to TypeScript, whereas the .ts
is for TypeScript compiling down to JavaScript. The .d.ts
is most commonly for declaring types in an npm
module for TypeScript to know, while .ts
is used for all your JavaScript files. The difference between .ts
and .tsx
is .tsx
is used for JSX files, like React components.
Another basic option to understand is exclude
. If you have a wide range in your exclude
, there might be certain files you don’t want the compiler to match. For instance, including everything is one option, but then you would probably want to put node_modules
under the exclude array.
See the official documentation for more about configuring your TypeScript compiler.
Once you have finished configuring your tsconfig.json
file, you can add a script in your package.json
to run the compiler. Under scripts, add something like, "tsc:w": "tsc -w"
. You can then run the script and it will point to your tsconfig.json
file and compile away!
@types
If you are using a frontend framework like React, you will need to also install @types
packages. Since everything in TypeScript requires type definitions, these packages like @types/react
will let TypeScript know the types for all the basic React classes, functions, components, etc.
This is needed to satisfy TypeScript, and rightly so. You will also need to remember to add types to any other library added to your project (e.g. @types/react-router-dom
, @types/react-bootstrap
, or @types/react-redux
).
Another example of the @types
packages is @types/node
. Without this package, keywords that we are used to, won’t be recognized in Typescript file (.ts
file).
For instance, using require
to import a package, would return an error like this: Error:(3, 12) TS2304:Cannot find name 'require'
. The package @types/node
defines this for TypeScript, so we are now allowed to use require
without errors.
Some JavaScript packages already have types written in them. These won’t require installing a types packages. However, what if there aren’t types written in the packages and there isn’t a types package to install?
First of all, this is hard to find, as TypeScript has a ton of support. However, if you use TypeScript widely, you will inevitably find a JavaScript package without TypeScript support.
The easy solution here is to simply let the TypeScript complier know your module exists. This won’t add TypeScript to the package, but it will bypass the TypeScript checker and allow you to use it. There are many ways to solve this, like fully adding TypeScript to the package yourself (lots of work in most cases), but this is just a simple solve.
Create a file and name it something like this: MyModuleDesc.d.ts
(the .d'ts
is the important part).
In that file, simply write declare module "my-module"
(with the “my-module” being the package that doesn’t have Typescript). Then, in your tsconfig.json
file, under the include
array, add the MyModuleDesc.d.ts
file (assuming this file is in the root of your project).
Alright, so enough about the basic how-to’s, let’s actually see this migration played out in live action. I have created a simple table for data I receive from Swapi.dev:
export default function TableComponent(props) { const { result } = props; return ( <Table> <Row><Cell>{result.name}</Cell></Row> <Row><Cell>Hair Color</Cell><Cell>{result.hairColor}</Cell></Row> <Row><Cell>Height</Cell><Cell>{result.height}</Cell></Row> <Row><Cell>Weight</Cell><Cell>{result.weight}</Cell></Row> <Row><Cell>Date of Birth</Cell><Cell>{result.dateOfBirth}</Cell></Row> <Row> <Cell>{result.filmNames.length <= 1 ? "Film" : "Films"}</Cell> <Cell>{result.filmNames.length === 0 ? "None" : result.filmNames.map((film, idx) => ( <LineItem key={'film' + idx}>{film}</LineItem>))} </Cell> </Row> </Table> ) }
This will make for a good example. Each element we see here is a styled-component
I have defined elsewhere; they are defined like this const Table = styled.div;
(with a bunch of CSS defined in between the quotes).
First off, this is using absolutely no TypeScript at all in my project. The file name is TableComponent.js
, and it is a child off of App.js to keep it simple. I originally initialized this project using create-react-app
— and you could easily do the same. The only package you need to follow along is styled-components
.
Next, to add TypeScript, you can install a few things through npm
:
npm i --save typescript @types/react @types/react-dom @types/styled-components
At this level, let’s run the command in the terminal:
npx tsc --init
This will create our tsconfig.json
file.
Now, you might notice an error in your tsconfig.json
file. It you hover over the very top first curly brace in that file, you will see the error:
“error TS18003: No inputs were found in config file tsconfig.json
. Specified include
paths were ["**/*"]
and exclude
paths were []
.”
I’ve noticed this error come and go at times, if you close out your editor and bring it back, but if you try running your app, you should see this error.
The basic idea here is we need at least one TypeScript file for the compiler to work. Let’s change the filename of TableComponent.js
to TableComponent.tsx
. You may need to restart your IDE, as I do while using Visual Studio Code, but you should see red lines under the props and in a few other spots, indicating an error.
We will get there, but first let’s try running it with this command:
npm run start
It will get pretty far, and you may even see the App component finish up, but our TableComponent
will show an error.
More specifically, you’ll see a Typescript error: “Parameter props
implicitly has an any
type. TS7006″.
This is exactly what the red lines are indicating. This is because noImplicitAny
is defaulted to true, and this is exactly what we want. We could go to our tsconfig.json
file and change it to false. Then, we would have no errors, but basically have plain JavaScript then.
One thing that’s important to note is that TypeScript ran on top of our current bundler without disturbing it.
We did an npm run start
even though we didn’t appear to call any TypeScript. Technically, with the noEmit
option set to true, TypeScript simply checked the types in our JSX, but our bundler emitted/output it. Another thing is to look at the tsconfig.json
file — it read our app and filled in some default options for us.
Let’s also note what TypeScript did in our project: it automatically created a file in our src
folder named react-app-env.d.ts
. This file isn’t too important, but you’ll notice, even if you delete it, it will automatically be generated on each start/build. It is a strange TypeScript thing that simply lets TypeScript know about react-scripts
types.
Without further ado, let’s work on changing this file to TypeScript.
We could easily make the red-line errors go away by adding : any
to the end of our variables, but that’s basically useless.
A better way, is to do what TypeScript is made for: checking the types to avoid errors. Let’s start by addressing the variables in our map function: “film”.
You will see these have red lines under them and are frankly the easiest to understand the types they require: they’re all strings, so we can easily go through and annotate: : string
on the end.
Then, we can annotate idx
with : number
, and it will satisfy the TypeScript compiler. All of this would be okay, but the problem remains with the props variable, which is still without annotation. And honestly, going through the props down in the JSX and annotating there isn’t the best way of doing things.
A better way is to create our own TypeScript interface or type for the props being passed in. Now, we would probably have done this in the parent App.js
component normally, when we fetched the result from an API (or even better, in the API as well if we had created it), but so you can see the flexibility of TypeScript, you don’t even need to do that.
Let’s create a type for our props:
type Props = { result: Result, };
Then the type for result:
type Result = { name: string, hairColor: string, height: string, weight: string, dateOfBirth: string, filmNames: string[], };
Then, simply annotate the props: props: Props
. Now, you should see the film is no longer red-underlined.
We could have made each of these interfaces instead of a new type, but as a general rule of thumb, try to make more controlled things (like props) a type. For more on choosing between type and interface, see this article.
This is by no means a complete guide to TypeScript. However, just to see a bit more of the basics, let’s look at styled-components
.
Right now we have no errors, but one of the great things about styled components is the ability to pass in a prop, so you can reuse the same definition and change it just a bit depending on the prop.
For instance, in our JSX, we want the first Cell (a styled component) to take up the entire width of our table, but the other Cells should only be half the width. So let’s put a “header” prop in there:
<Cell header> {result.name} </Cell>.
TypeScript doesn’t like this and doesn’t know what to do with it. Here’s what my current definition for Cell is:
const Cell = styled.div` padding: 5px 20px; display: flex; border: 1px solid black; flex-direction: column; justify-content: center; align-items: center; width: 50%; `;
Normally, we could add our prop to the style we create like this:
width: ${props => props.header ? '100%' : '50%}
But we will see header here is also an error according to TypeScript, because we haven’t defined it.
We need to create a new type for Cell props, like this:
type cellProp = { header: boolean, };
Now add it to the Cell component we create:
const Cell = styled.div < cellProp > ` ... width: ${(props) => (props.header ? "100%" : "50%")}; `;
This will clear up the errors. Then we get something else: every other instance of Cell gets an error because it doesn’t have a header prop.
This may seem strange, but it is expected behavior — we are telling TypeScript each instance of Cell needs this header boolean.
We could go through and make each instance have a prop header={false}
, but that’s extra clutter in our JSX.
A better fix is to make the header prop optional. We could do that easily by adding a question mark in the type definition:
type cellProps = { header?: boolean, };
Viola! No more errors.
This was a little taste of migrating a React component over to TypeScript. As you can see, migrating over to TypeScript is not much of a hassle at all. It is flexible, so you can use it on the files you need it for, while waiting on the files that have less priority. And before you know it, you can have TypeScript working in your project!
For more information, see the official docs.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]