Navigate back to the homepage
Browse Repo

How to Write Better TypeScript

Ovie Okeh
July 31st, 2020 · 8 min read

How to write better TypeScript

TypeScript: love it or hate it, you can’t deny the fact that it’s spreading like wildfire. In fact, according to the Stack Overflow 2019 developer survey, it was listed as the third most-loved programming language and the fourth most-wanted.

Now whether the survey accurately represents real-world sentiments is up for debate, but the fact remains that TypeScript is here, and it has come to stay. It’s so pervasive now that it has replaced vanilla JavaScript as the language of choice for many packages in the JS ecosystem, with some like Yarn even going as far as rewriting their entire codebase in TypeScript.

I feel one of the reasons for this meteoric rise to success has to be the fact that TypeScript, in essence, is just JavaScript. This makes the entry bar a lot lower for existing JavaScript developers, and the fact that it’s typed may also attract other devs who prefer the features typed languages provide.

This cuts both ways, too, because the ease of picking up TypeScript has led to some cases where the language is not being utilized as effectively as it could be. Many developers still write TypeScript like they’re writing JavaScript, and this brings with it some disadvantages.

We’ll be looking at some real-world code written in TypeScript that could be improved to make better use of the language’s strengths. This is by no means an exhaustive list, and I welcome you to list some you might have noticed in the comments section below.

Some of these examples involve React because I’ve noticed some instances where React code could be improved by simply making use of some TypeScript features, but the principles are by no means limited to React. Let’s dive in.

NOTE: Many code snippets in this article have been taken from real-world projects and anonymized to protect their owners.

Interface neglect

Let’s start with one of the most useful features of TypeScript: interfaces.

In TypeScript, an interface simply specifies the expected shape of a variable. It’s as simple as that. Let’s look at a simple interface to drive the point home.

1interface FunctionProps {
2 foo: string;
3 bar: number;

Now if any variable is defined to implement FunctionProps, it will have to be an object with the keys foo and bar. Any other key addition will make TypeScript fail to compile. Let’s look at what I mean.

1const fProps: FunctionProps = {
2 foo: 'hello',
3 bar: 42,

Now we have an object fProps that implements the FunctionProps interface correctly. If I deviate from the shape specified in the interface by, say, writing = 100 or deleting, TypeScript will complain. fProps’s shape has to match FunctionProps exactly or there will be hell to pay. = true // foo must be a string

Now that we’ve gotten that out of the way, let’s look at an example. Take this React functional component method:

1const renderInputBox = (props, attribute, index) => {
2 return (
3 <div key={index} className="form-group">
4 {renderLabel(attribute)}
5 <InputBox
6 name={attribute.key}
7 value={!!isAssetPropAvailable(props, attribute) && props.inputValue}
8 onChange={props.handleInputChange}
9 placeholder={`Enter ${attribute.label}`}
10 />
11 </div>
12 );

While this is completely fine if you were writing JavaScript, it doesn’t take advantage of interfaces. Why is this bad? For one, you don’t get any IntelliSense features that you otherwise would if the method’s arguments were typed.

Also, you could easily pass in a prop of a different expected shape to this method and you would be none the wiser because TypeScript would not complain about it.

This is just vanilla JS, and you might as well eliminate TypeScript from the project altogether if everything was written like this.

How could we improve this? Well, take a look at the arguments themselves, how they’re being used, and what shape is expected of them.

Let’s start with props. Take a look at line 7 and you can see that it’s supposed to be an object with a key called inputValue. On line 8, we see another key being accessed from it called handleInputChange, which, from the context, has to be an event handler for inputs.

We now know what shape props is supposed to have, and we can create an interface for it.

1interface PropsShape {
2 inputValue: string;
3 handleInputChange: (event: React.FormEvent): void;

Moving on to attribute, we can use the same method to create an interface for it. Look at line 6. We’re accessing a key called key from it (hint: it’s an object). On line 9, we’re accessing another key from it called label, and with this information, we can go ahead and create an interface for it.

1interface AttributeShape {
2 key: string;
3 label: string;

We can now rewrite the method to look like this instead:

1const renderInputBox = (props:PropsShape, attribute:AttributeShape, index:number) => {
2 return (
3 <div key={index} className="form-group">
4 {renderLabel(attribute)}
5 <InputBox
6 name={attribute.key}
7 value={!!isAssetPropAvailable(props, attribute) && props.inputValue}
8 onChange={props.handleInputChange}
9 placeholder={`Enter ${attribute.label}`}
10 />
11 </div>
12 );

Is it more code to write? Yes. But consider the benefits of doing this:

  • You get IntelliSense wherever you use this method, and you can instantly see what its arguments are supposed to look like without having to look at it.
  • You can never misuse this method because TypeScript will not allow you to pass in arguments with wrong shapes.
  • Any change to the method definition — maybe index is now a string — and TypeScript will prevent your code from compiling until you fix all the instances where the method was used.

Why use TypeScript in the first place if you don’t care about these benefits?

Any abuse

The type any is a fantastic way for you to migrate an existing JavaScript project gradually to TypeScript.

Why is this? Well, if you type a variable as any, you’re telling TypeScript to skip type-checking it. You can now assign and reassign different types to this variable, and this allows you to opt-in and out of type checking when necessary.

While there may be other use-cases for using any, such as when you’re working with a third-party API and you don’t know what will be coming back, it is definitely possible to overuse it and, in effect, negate the advantages of TypeScript in the process.

Let’s take a look at a case where it was definitely abused.

1export interface BudgetRequiredProps {
2 categoryDetails?: any[];
3 state?: any;
4 onInputChange?: (event) => void;
5 toggleSubCategory?: (type: any) => any;
6 displaySubCategory?: () => any[];

This interface breaks my heart. There are legitimate use cases for any, but this is not one of them.

For instance, take a look at line 2, where we’re basically specifying an array that can hold content of any type. This is a bomb waiting to explode wherever we’re mapping over categoryDetails, and we don’t account for the fact that it may contain items of different types.

NOTE: If you need to work with an array that contains elements of different types, consider using a Tuple.

Line 3 is even worse. There is no reason why state’s shape should be unknown. This whole interface is basically doing the same thing as vanilla JS with regards to type checking, i.e, absolutely nothing. This is a terrific example of interface misuse.

If you have ever written an interface like this in production code, I forgive you, but please do not let it happen again. Now, I went through the codebase where this example was plucked from to look at the expected shapes of the variables, and this is how it should look:

1export interface BudgetRequiredProps {
2 categoryDetails?: CategoryShape[];
3 state?: string | null;
4 onInputChange?: (event: React.FormEvent) => void;
5 toggleSubCategory?: (type: string) => boolean;
6 displaySubCategory?: () => CategoryShape[];

Much better. You get all the advantages of using TypeScript without changing the interface too much. Now let’s take a look at where using any actually makes sense.

1export interface WeatherPageProps {
2 getCurrentWeatherStatus: (city: string): Promise<any>;
3 handleUserUpdate: (userContent: any): Promise>any>;

Why is this a valid use case for any?

Well, for one, we’re working with an external API. On line 2, we’re specifying a function that makes a fetch request to a weather API, and we don’t know what the response should look like; maybe it’s an endpoint that returns dynamic data based on certain condition.

In that case, specifying the return type as a promise that resolves to any is acceptable.

NOTE: This is not the only approach to working with dynamic data. You could specify all the possible values coming from the endpoint in the interface and then mark the dynamic fields as optional.

On line 3, we’re also working with a function that takes in a prop that is dynamic in content. For instance, say userContent comes from the user, and we don’t know what the user may type. In this case, typing userContent as any is completely acceptable.

Yes, there are valid use-cases for the any type, but please, for the love of TypeScript, avoid it as much as you possibly can without ruining the developer experience.

Index signature misuse

Now, this is a very subtle mistake I see quite a lot in React code where you may need to map over an object and access its properties dynamically. Consider this example:

1const obj = {
2 gasoline: 'flammable',
3 sauce: 'hot',
4 isTypeScriptCool: true,
6Object.keys(obj).forEach(key => console.log(obj[key])) // 'flammable', 'hot', true

The above example will not cause an issue with vanilla JavaScript, but the same is not true in TypeScript.

1interface ObjectShape {
2 gasoline: string;
3 sauce: string;
4 isTypeScriptCool: boolean;
6const obj: ObjectShape = {
7 gasoline: 'flammable',
8 sauce: 'hot',
9 isTypeScriptCool: true,
11Object.keys(obj).forEach(key => console.log(obj[key])) // you can't just do this

The reason you can’t just do that is because of type indexing.

In TypeScript, you need to specify how an interface should be indexed into by giving it an index signature, i.e, a signature that describes the types we can use to index into the interface, along with the corresponding return types.

A quick refresher: indexing into an object looks like obj[‘sauce’] or obj.gasoline

We didn’t tell TypeScript what index signature ObjectShape should have, so it doesn’t know what to do when you index into an object that implements it as we do on line 13. But how does this concern React?

Well, there are cases where you might need to iterate over a component’s state to grab certain values, like so:

1interface ComponentState {
2 nameError: string;
3 ageError: string;
4 numOfFields: number;
6this.state: ComponentState = {
7 nameError: 'your name is too awesome',
8 ageError: 'you seem immortal',
9 numOfFields: 2,
11Object.keys(this.state).forEach(err => this.handleError(this.state[err]));

This is a very common operation in React, but you can see how we may run into a problem on line 13. We’re indexing into this.state, but the interface it implements doesn’t have an index signature. Oops.

But that is not even the mistake I’m talking about, and I’ll get to it in a moment. To fix the warning TypeScript throws, you might update the state’s interface like so:

1interface ComponentState {
2 nameError: string;
3 ageError: string;
4 numOfFields: number;
5 [x: string]: any; // index signature added

Before we continue, it’s worth noting that, by default, adding an index signature to an interface also means you will be able to add new values that do not exist in the interface to any variable that implements it.

This will successfully get rid of the error, but now you’ve introduced a new side effect.

This is the equivalent of telling TypeScript that when ComponentState is indexed with a string, it should return a value of type any (basically all possible types). This could cause issues if this.handleError was not expecting anything apart from a string or a number.

But more importantly, you can now add a new property with ANY type to whichever variable implements the interface, which, in our case, is this.state. So this becomes valid:

1this.state['shouldNotBeHere'] = { bugs: 1, dev: 0 }

Now that is the mistake I am talking about. How do we fix it, though? Well, there are actually two things we need to look out for:

  • We want to specify all possible index return types in the object, but no more (no any)
  • We don’t want to be able to add new values to an object because of indexing So, in most cases, the proper way to fix our initial issue (indexing into an object without TypeScript complaining) would be to do this:
1interface ComponentState {
2 nameError: string;
3 ageError: string;
4 numOfFields: number;
5 readonly [x: string]: string | number;

OK, so here’s what this piece of code is saying:

Hey, TypeScript, I would like to be able to index into this interface with a string, and I should get either a string or a number back. Oh, and please don’t let me add any other thing to any object that implements this interface that I didn’t explicitly specify.

By simply specifying the index signature return values, we’re able to solve our first issue, and by marking it as readonly, we’re able to solve the second issue. Please watch out for this subtle issue when writing TypeScript code.


TypeScript is a wonderful way to write type-safe JavaScript, but you have to do it right. It’s possible to write TypeScript in a way that just causes headaches with no real benefit, but thankfully, that can be easily solved by taking the time to learn the gotchas of the language.

I hope you were able to learn one or two things from this article, and if you have some examples you’d like to share, please add them in the comment section below so that others can benefit.

Goodbye and happy coding!

About the Author

Ovie is a frontend developer with an eye for detail and proven expertise in turning ideas into performant, fully-functional software. Read the original article or more interesting posts on Ovie’s blog.

Frontend Monitoring

OpenReplay is a frontend monitoring tool that replays everything your users do and shows how your web app behaves for every issue. It’s like having your browser’s inspector open while looking over your user’s shoulder.

OpenReplay Frontend Monitoring

OpenReplay lets you reproduce issues, aggregate JS errors and monitor your web app’s performance. Happy debugging, for modern frontend teams - Start monitoring your web app for free.

More articles from OpenReplay Blog

Incorporating CSS to a Page via HTTP Headers

A quick solution to use CSS via HTTP headers. Tips & Techniques on how to add CSS to pages on personal projects.

July 29th, 2020 · 5 min read

Getting Started with Nuxt.js

Take a look at Nuxt framework, how to set it up and also how it is structured in building Vue powered applications.

July 24th, 2020 · 6 min read
© 2021 OpenReplay Blog
Link to $ to $ to $