Typescript Mistakes To Avoid
By: Mateusz Ledwoń
Tags: Typescript, JavaScript, Programming, Frontend, Front End Development

Typescript took the Javascript world by storm. If you are a frontend developer working with the browser or a backend dev using Node (or Deno) there is a huge possibility that you have already worked with or will be working soon with a codebase written in Typescript. No matter what level of expertise you are on in a programming language — in your day to day development you will make mistakes. The worst thing, however, is making them because you are not aware that you are doing something wrong. Here is a list of common Typescript bad practices that you should make sure to avoid.

Skipping type safety

Using any instead of unknown

We are told that any is something that should be avoided at all costs. And this is right, any is basically a hack that disables the type checks and lets us do whatever we want with a value: pass it as a parameter to any function, assign to any variable, call any methods on it, etc. The downsides of this are obvious. There are, however, places in our applications where we truly cannot be sure what value will hold. For example, when we are dealing with some external input or a call to a backend API. And these are the places where unknown type fits best. It is similar to any in the way that we are able to assign any type of value to it but differs in some important aspects.

interface SomeType {

a: number;

} const a: unknown = JSON.parse("{'a':1}");

const someFn = (input: SomeType) => {

//

};

someFn(a); // Argument of type 'unknown' is not assignable to parameter of type 'SomeType'.

We cannot just start using this unknown value and i.e. pass it to a function expecting an object of certain shape, the type system will force us to first make sure if it has a correct type. For this purpose we can use any of the type narrowing mechanisms offered by the language ( instanceof , typeof , type guards) or we need to perform a type assertion which at least will make our intentions explicit:

const isSomeType = (value: unknown): value is SomeType =>

value.hasOwnProperty('a'); if (isSomeType(a)) {

someFn(a); // this is fine

}

Using type assertions instead of type declarations

There is a huge difference between const dog: Dog = { breed: 'beagle' } and const dog = { breed: 'beagle' } as Dog . It probably won’t be a surprising information for any seasoned Typescript developer but I see the second one used as a crutch in code pretty often. Basically in the first example we are telling Typescript to check if our variable complies with the specified type and in the second one we tell the type checker that we know better and it can skip the check. It is then similar to any in a way it offers us an escape hatch from the type system. It is, however, a bit less dangerous because we will get an error if we try to assert something unreasonable, i.e. writing undefined as string will result in an error:

Conversion of type 'undefined' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

The worst we can do in this scenario is to use the suggestion from the second part of this error message and go with someValue as unknown as someType which will silence the type checker.

Using the Function type

It might be tempting, especially if we are working with code that deals with callbacks of various shapes, different parameter lists, etc. to use simple:

const acceptsCallback = (callback: Function) => {

callback();

};

This gives us a way to handle all scenarios with one type that we can call with anything we want. As you probably suspect this is a hacky way of doing things. We can call a value of type Function but we will be dealing with an untyped function call because the return type will always be any . If you feel the need to use this kind of solution this is probably a signal that you should restructure your code to be able to provide a stronger type information.

Unnecessarily iterating over object properties

This is another way that lets any type creep into our code. We would like to iterate over object properties:

interface Dog {

name: string;

breed: string;

age: number;

} const dog = { name: 'Andy', breed: 'beagle', age: 7 }; for (const key in dog) {

const value = dog[key] // this becomes an implicit any

}

What is happening here? If we would did something like const name = dog['name'] the name variable would have a correct number type. However, if we try the same thing in a loop something different happens. We are not referencing the property value by a narrow 'name' literal which type checker knows is an existing property of the Dog type but by using more general string type. We could fix it by using type assertion const value = dog[key as keyof Dog] but we already know that this is not ideal. This might look like strange behavior but Typescript is doing the right thing. Every object in Javascript can have additional unknown properties that can hold values of any type. When we use Object.entries we are getting the same: string keys and any values. This means that iterating over object keys should be a tool that we don’t overuse. If we know that we are dealing with truly dynamic data we can use an index signature and if we need a data structure that models a key/value collection we have the ES6 Map for that.

Writing or skipping types in wrong places

Adding types that can be inferred

Some might say that the things pointed out in this section are issues with the code style rather than actual errors. In my opinion, the line between those two areas is often pretty blurry and the amount of errors that can be prevented by following a good style guide might sometimes be more impressive than i.e. by religiously avoiding type assertions.

The first point in the section is strictly related to the code readability. In the Typescript world we can have implicit and explicit types:

const a: number = 2;

const b = 2;

The rule of thumb should be: always avoid adding types where they can be inferred. Redundant type annotations add more noise and clutter your code which makes it unnecessarily verbose and harder to read. It also makes refactoring more painful.

Not annotating object literals

We introduced our rule above but what rule would it be without some exceptions? Here is the first one. It is very useful to annotate type when we are defining an object literal. It becomes even more useful if it is a complex one.

interface Owner {

name: string;

address: string;

} interface Dog {

name: string;

breed: string;

age: number;

owner: Owner;

} const dog = { name: 'Andy', breed: 'beagle', age: 7 };

In the above example we forgot about the owner property but we obviously don’t get any error cause there is no connection with the dog literal and the Dog interface. We will get an error when we will try to use our dog by i.e. passing it to a function. This is ok but a much better option would be to keep this error closer to its source by adding an annotation here const dog: Dog = { name: ‘Andy’, breed: ‘beagle’, age: 7 }; . This time we will be immediately hit with Property ‘owner’ is missing in type ‘{ name: string; breed: string; age: number; }’ but required in type ‘Dog’ and forced to fix it right away instead of dealing with it when we work on some other code five files away.

Not specifying function return types

This is another exception to our rule. We should add a return type when we are defining a function. We always need to define types for function parameters. If we don’t, we will be dealing with any but we are not forced to define a return type and Typescript is pretty good at inferring it.

const compareLength = (a: string, b: string) => {

return a.length > b.length;

};

The above example shows very well that we will be getting a boolean . However adding a type annotation here can be beneficial. If we are dealing with more complex function we will be able to just have a look at it and have a pretty good idea what it does without diving into its body. Also when we are writing this function we get an early feedback from the compiler that lets us know if we are returning what we think we are.

Using wrong tools for the job

Using numeric enums

Enums in Typescript are a bit unintuitive and problematic for a couple of reasons but let’s focus on the most important of them. When we create a simple enum by default we get a numeric one with implicitly initialized members.

enum State {

InProgress,

Success,

Fail

} State.InProgress; // (enum member) State.InProgress = 0

State.Success; // (enum member) State.Success = 1

State.Fail; // (enum member) State.Fail = 2

And there is a surprising behavior related to that. If we have a function like the one below we won’t get any error if we call it with a number that is not in the range of our enum:

const fn = (state: State) => {

//

}; fn(100);

Alternative for this is to use string enums with explicitly initialized members or even better to use a union of string literals type State = ‘InProgress’ | ‘Success’ | ‘Fail’; .

Using wrapper objects instead of primitives

This one can be a bit confusing if you are not familiar with Javascript idiosyncrasies related to its primitive types number , string , boolean , etc. It’s super easy to annotate something erroneously with the uppercase version of one of them:

const fn1 = (str: String): String => {

return str;

}; const fn2 = (str: string): string => {

return str;

}; fn2(fn1('abc'));

Above code will fail with:

Argument of type 'String' is not assignable to parameter of type 'string'. 'string' is a primitive, but 'String' is a wrapper object. Prefer using 'string' when possible.

Which is correct because String and string are not equivalent. Also Typescript suggests a proper solution. We should always avoid those uppercase types (boxed or wrapper objects) because they are just a Javascript-specific way to provide some methods on primitives. We usually don’t need and shouldn’t use them directly.

Relying on private to hide information

When working with classes a common thing that you would like to do is to restrict access to some field. Typescript has a way to do something like that with its private keyword.

class Dog {

name: string;

private age: number; constructor(name: string, age: number) {

this.name = name;

this.age = age;

}

} const dog = new Dog('Andy', 8);

dog.name;

dog.age;

Calling dog.age will give us Property ‘age’ is private and only accessible within class ‘Dog’ . This functionality is cool and has some usages but we should be aware of an important aspect of it: it is only being enforced during compile time. The private annotation will disappear in runtime and the property will be fully accessible and visible. (dog as any).age will just work. To restrict access in runtime we can use i.e. new Javascript private field feature #age .

Duplicating types

Repeating interfaces

This section deals with features that might be a little harder to grasp for newcomers to the strongly typed languages. Typescript gives us the ability to create types from existing types by applying some operations to them.

The easiest case is the situation where we have some properties common to two interfaces:

interface Animal {

age: number;

name: string;

group: string;

} interface Dog {

breed: string;

age: number;

name: string;

group: string;

}

In this scenario we are dealing with two interfaces where one of them is clearly an extension of the other. To avoid duplication and potential problems if we i.e. decide at some later stage that the we will change the group to be a union of literal types we should use the extends keyword:

interface Animal {

age: number;

name: string;

group: string;

} interface Dog extends Animal {

breed: string;

}

Sometimes, however, we see that we should share some part of the type but the relationship between our interfaces looks more like a relationship between a superset and a subset:

interface Colors {

red: '#ff0000';

orange: '#ff9900';

yellow: '#ffff00';

blue: '#0000ff';

grey: '#cccccc';

} interface WarmColors {

red: '#ff0000';

orange: '#ff9900';

yellow: '#ffff00';

}

Typescript also gives us a tool to model things like that. We can use its utility type Pick : type WarmColors = Pick<Colors, ‘red’ | ‘orange’ | ‘yellow’> which is built on top of the mapped type concept (read more about Pick here).

Repeating properties or value types

Another situation where we might introduce a dangerous duplication is when we want to reference a type of some specific property of another interface.

interface Box {

color: string;

dimensions: {

a: number;

b: number;

c: number;

};

} interface Dimensions {

a: number;

b: number;

c: number;

}

We could just reverse this and do:

interface Dimensions {

a: number;

b: number;

c: number;

} interface Box {

color: string;

dimensions: Dimensions;

}

But what if i.e. we are not in control of the Box interface because it is provided by some external library? What we can do is index into this type by using type Dimensions = Box[‘dimensions’] . This is especially useful when we are dealing with tagged unions and we want to compute the type of a tag:

interface Admin {

role: 'admin';

id: string;

} interface RegularUser {

role: 'regular_user';

id: string;

name: string;

} type User = Admin | RegularUser;

type UserRole = User['role']; // 'admin' | 'regular_user'

Instead of referencing types we may also want to reference properties of some other interface. Let’s say we want to use a function that accepts one of the keys of some interface. We could list all the keys by hand:

interface Dog {

name: string;

breed: string;

age: number;

owner: Owner;

} const someFn = (dogProperty: 'name' | 'breed' | 'age' | 'owner') => {

//

};

But again we are repeating ourselves and introducing potential bugs if the Dog interface gets additional properties later. What we should do instead is to use the keyof Dog operator that gives us exactly what we need here.

Not using strict mode

Not checking your nulls

All mistakes from this section are easily avoidable if we enable the --strict mode in our compiler configuration. We should always do that but sometimes when we are dealing with an existing project we don’t have this luxury and we need to do it gradually. The strict mode consists of several options that can be enabled one at a time.

Probably the most important of them is the --strictNullChecks option that will let us avoid a lot of runtime errors. The idea is simple: without it we are allowed to assign undefined or null to any type. Consider the following code:

const element = ['el1', 'el2', 'el3'].find((el) => el === 'el4'); const someFn = (el: string) => {

//

}; someFn(element);

Without the --strictNullChecks this is completely legal even though find in our case returns string | undefined . With the flag enabled we would get: Argument of type ‘string | undefined’ is not assignable to parameter of type ‘string’. Type ‘undefined’ is not assignable to type ‘string’ .

Not checking the bind, call and apply types

Another one is --strictBindCallApply . Without this option enabled calls to bind , call and apply return any . Also we get no type checks when we use them with wrong arguments. When we have it on, all of the examples below result in type errors:

const someFn = (first: string, second: string): boolean => {

return first.length > second.length;

}; someFn.apply(undefined, ['abc', 'defg', 'hijkl']); // Argument of type '[string, string, string]' is not assignable to parameter of type '[first: string, second: string]' someFn.call(undefined, 1); // Argument of type '1' is not assignable to parameter of type 'string' someFn.bind(undefined)('abc'); // Expected 2 arguments, but got 1

Using implicit any and implicit this

The --noImplicitAny is an important one. If you have it on, you would be able to catch a lot of errors mentioned in this article. It basically won’t let you introduce the any type in your code if it is not being explicitly annotated as any . This is super helpful for all the cases where we i.e. might assume the correct inference of our type but we overestimate the Typescript abilities.

const someFn = (first, second): boolean => {

//

};

The above code will result in Parameter ‘first’ implicitly has an ‘any’ type error.

Another problem that can be solved by enabling a flag is related to the way this works in Javascript. Basically sometimes we are unable to tell upfront what will be the actual type of it and the type checker can catch those cases for us. We use --noImplicitThis for that.

const dog = {

name: 'Andy',

sayName: function () {

return function () {

console.log(this.name);

};

}

}; dog.sayName();

The above will result in a ‘this’ implicitly has type ‘any’ because it does not have a type annotation error. One way to fix it is to turn the innermost function into an arrow one () => { console.log(this.name) } .

Summary

Hopefully reading this will let you avoid some of the most common pitfalls that are waiting for the Typescript developer. The topic itself is huge and a list like that cannot claim to be completely exhaustive so you should keep exploring. I used a lot of various resources when writing this article and here are some of the best ones that are worth checking out:

Official Typescript documentation (obviously): https://www.typescriptlang.org/docs

Effective Typescript by Dan Vanderkam (invaluable resource): https://effectivetypescript.com

Marius Schultz’s blog (check out also his Typescript Weekly newsletter): https://mariusschulz.com

Axel Rauschmayer’s blog (every JS/TS developer should know it by now): https://2ality.com