So, I've been working on a company project for almost half a year now. After feature development was finished and work on it slowed down, I decided I wanted to take the next step, which I did not dare take 6 months back, and write (or in this case "migrate") the project to Typescript.
These are all the things I learned in the process and, in retrospect, I wish I knew when I was starting the migration.
Disclaimer 📣
This post assumes you are a beginner with TS but its purpose is not to teach you TS. Its purpose is to give some advice to the next person who wants to try something similar and make their lives easier.
This post assumes that you are working on a CRA project and most of the setup and configuration is already been taken care for you. However most of the advice could be applied on any React project.
This post is based on my experience with migrating a moderately big codebase to TS.
Before you begin ⚙️
Set allowJs
in your tsconfig to true
That's all. This will ensure that you are not drowning in red the moment you enable the type checker and will allow you to migrate gradually; letting you keep your existing .js
files, until their time comes.
Create a global.d.ts
file in your src
directory
This will come in handy, since you most likely will want to augment or extend the types of existing libraries you are using.
Create a custom.d.ts
file in your src
directory
You will need this, in order to be able to import files that are not code modules, such as image files, .json
files, etc. To help you get started, just add this inside your custom.d.ts
file:
declare module '*.svg' {
import React = require('react');
export const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
const src: string;
export default src;
}
declare module '*.json' {
const content: object;
export default content;
}
Et voila, now you can import .svg
and .json
files inside your code modules without TS throwing errors at you.
Should you want to enable noImplicitAny
and no-explicit-any
do it before you start
noImplicitAny
is a compiler option which will raise errors on expressions and declarations with an implied any
type.
no-explicit-any
is an eslint
rule that doesn't allow you to define anything as any
type.
If you do enable these options, the unknown
type will be your friend.
These options should ideally be enabled from the get go. No matter what, do not enable them during the migration process. I made that mistake and ended up suddenly having to spend lots and lots of time resolving quite an amount of errors that I "inflicted" upon myself. Can be quite disheartening while you are still in the middle of the migration.
Setup your eslint
config for use with TS
I happened to have an issue with eslint
's default no-unused-vars
rule and TS. Turns out there is a specific no-unused-vars
rule for TS. Enable this and disable the default one
Settle on how you will be defining your types
Will you be using interfaces or types? Will you be writing your types inside the code file or as a separate file? I advise that you settle on these details before you start because you don't want to change you mind halfway there and have to retrofix all the files you have already worked on.
My advice is use types unless an interface is absolutely necessary and keep your type definitions separate from the component files themselves.
The system I applied was the following:
- If the file is a React component file, create a separate
types.d.ts
file in the same directory and put all the type definitions there. Except for theProps
type definition; I keep that in the component file itself, since it is handy to have the props definition readily available. - If the file is not a React component type, declarations go along with the code. No separate
types.d.ts
file. That is unless the types are so many that make the code file really messy, then they are taken out in their own file.
Read the documentation
Seriously. At least read some of the important parts, such as:
- Basic types
- Generics
- Advanced types - Especially how intersections and unions work, because they do not map exactly 1:1 with the mathematical definition of unions and intersections
- Type compatibility
- Utility types - Those are very handy, give them a look to, at least, know of their existence.
- Release notes - I have found that sometimes things mentioned in the release notes of a new version are not mentioned in the docs. For example this very handy assertion function functionality which was added in version 3.7 and AFAIK is not mentioned anywhere in the docs.
Prepare to start being more thorough when you code
Typescript will eventually lead you to be more thorough with some assumptions you make while coding. Assumptions for which, in your mind, know to be correct for your code, are not enough for TS. Typescript will always be asking to validate those assumptions by writing some more code, mostly in the form of
if (notCondition) {
throw new Error()
}
// Otherwise all is well
You'll get used to it, and eventually it will come to you naturally.
Get ready to be wrong
Accept the fact that 99.999% of times the TS compiler will be right and you will be wrong 😅
On with the actual migration 🚀
Start small
When you make the transition from JS to TS you want to start small. See that directory with your utility functions? They are by far the simplest things you can start migrating over to TS.
Once you migrate a simple function see where this function is used, what other files import it (the compiler will probably let you know, by throwing some errors at you at this point).
Proceed with migrating those files, and repeat the process, essentially climbing up (down?) the dependency tree. If you reach a file that is far too complex for this stage of the migration don't be afraid to put a @ts-nocheck
directive at the first line of it, and migrate it at a later time.
Do not be afraid to do type assertions
Type assertions in TS are like type casting in other languages. In essence you are telling the TS compiler that you know better, and a specific variable, even though it could possibly be of many types, cannot be anything else other than a specific type in this case. Sometimes you do in fact know better. But only sometimes 😅
I have found this to be useful when working with React hooks that provide some context value. Context values start with a "default" value when you initialise the Context
, which may not always be compatible with the actual value passed to the provider. More on that in this post.
Custom type guards and assertion functions are also very useful in helping you help TS make the correct type assertions
Do not use the provided FC
type for React components
I highly encourage you to not define React components like this
const Comp = FC<Props>(props) => { .... }
I found its usage to be more of a bother than anything, since it makes some rather arbitrary assumptions about your components (like always having children
props and not working very well with PropTypes
) and in general takes away some of the control you have over defining your components the way you want. Use
function Comp(props: Props) { .... }
instead. TS is smart enough to infer the return type itself, and for all intents and purposes this is a valid React component, which you can use in any case where a React component is expected by the compiler.
Keep your PropTypes
While TS is very useful in making sure you are not making any type related mistakes during development time, PropTypes
are very useful in letting you know of type related mistakes during runtime. How can you have type related errors during runtime, if you are using TS for development, you ask? Consider this scenario:
You have defined your API response type in TS as bringing back a field which is supposed to be a number
. You have also defined your relevant PropTypes
field as such. All is well.
Now, imagine if your API returns a string
instead of a number
in that field. Had you removed the PropTypes
you would never realise the error early, until the app crashed at some point. With PropTypes
you will get a very useful warning in the browser console if any such mismatch ever occurs.
If you work on an ejected CRA project, or otherwise you have access to the babelrc
file, know that a plugin exists that can automatically convert your TS types to PropTypes
, so that you don't have to manually update both.
Export all your types
Even if you don't end up importing all of them in other files, make a habit of exporting them since you never know when you may need a type which is defined in another module.
Do not be afraid to use generics
TS generics can be really helpful and you can have generic React components as well. For example
// Comp.tsx
type Props<T> = {
result: T;
}
function Comp<T>(props: Props<T>) {
// T can also be used inside the function body too, if needed
}
// OtherComp.tsx
type SomeType = ...
function OtherComp() {
return (
<Comp<SomeType> someProp={...}>
<SomeContent />
</Comp>
)
}
In my experience, if a React component is an abstraction over some thing, then it's safe to assume that this component's type definition will be a generic.
Read the type definitions of libraries you use
When in doubt always read the type definitions for the libraries you use. This will also help you on how you should define your own component types for usage with components from external libraries.
You don't always have to define the return types
TS is (mostly) smart enough that it will correctly infer the type of the return values of functions. Personally, I like to define return values too, but that's because I have some kind of OCD 😅
Do note that there may be times that you'll have to strictly define the return value for things to work correctly.
Make type definitions for your API responses
They will help you immensely, since it's more than likely that you will be using server provided data in parts of your application
Learn to read TS compiler errors
TS errors can be intimidating, however there is a "cheat" in how to read them. Always read the first and the last few lines of the error message. If you still don't make sense do read the whole error, but usually just reading those lines will give you the information you need to make sense of the error.
Random Typescript tip ✅
Do (TypeA | TypeB)[]
don't TypeA[] | TypeB[]
When you have an array whose type may be either an array of TypeA
or an array of TypeB
declare it as
const arr: (TypeA | TypeB)[]
instead of
const arr: TypeA[] | TypeB[]
There is a subtle difference between those 2 declarations and the second one will lead to errors if you try to .map()
over the arr
Closing words
The fact that you decided to migrate your app instead of writing it in TS from the start, may conceal the merits of having used TS at the beginning, since your whole code base is already laid out.
However you will see the merits once you continue development and now all your new code will have to abide by the TS rules.
Remember, your code is now quite a lot more thorough with handling error cases.
This, combined with some proper tests, will ensure that your code is as robust as it can be.
Thank you for reading! 🎉
Top comments (0)