James Garbutt

Software engineer. Front-end @crispthinking.

Avoiding the TypeScript `any` type

February 01, 2019

Edit Page

Lately, I’ve been spending a bit of time moving a few different projects to use strict: true in the TypeScript compiler, as well as enabling a large amount of lint rules.

One of these rules was no-any.

While this does mean everyone now hates me, it has also lead to people on these projects learning and understanding the type system of TypeScript much more than they previously did.

So here’s a few things we learnt by avoiding the any type…

It isn’t bad

The any type isn’t necessarily a bad thing and, in actual fact, does still come in useful sometimes. However, in most cases, there is a better alternative that leads to having better defined types overall.

Anyhow, with that out of the way…

unknown can usually be used instead

The unknown type is relatively new, introduced in TypeScript 3.0.

It is similar to any in that it can be “anything”, but has one huge difference: it can only be interacted with once we know the type (through something like a type guard or inference).

A quick example:

const foo: any = "foo";
const bar: unknown = "bar";

foo.length; // Works, type checking is effectively turned off for this
bar.length; // Errors, bar is unknown

if (typeof bar === 'string') {
  bar.length; // Works, we now know that bar is a string
}

In most cases, you can do the ol’ switcheroo and unknown will work fine where you once had any. Just be ready for a painful fair amount of changes needed to add type guards or casts throughout your code…

Record can be used for basic objects

A common thing i’ve seen is the use of any in parsing API responses (e.g. JSON) as the type of the objects may not always be known.

More than likely, such objects are… objects. So give Record a try instead:

const foo: any = { a: 1, b: 2 };
const bar: Record<string, number> = { a: 1, b: 2};

foo.a; // Anything
bar.a; // Number

const obj = JSON.parse(response) as Record<string, unknown>[];
obj[0].id; // unknown but we can at least access it correctly

As you can see, it can work well to combine this with unknown too if you have more complex objects and you’ll at least have better than any.

Explicit types are easier to understand and read

This is more about types in general than just any, but we often had cases like this:

function doSomething(obj: Model) {
  if (obj.x) {
    return doAnotherThing(obj);
  }
  if (obj.y) {
    return obj.y;
  }
  return null;
}

This isn’t a great example, but it is already difficult from a glance at the function to know what it returns.

If we instead had:

function doSomething(obj: Model): XModel|YModel|null {

It is much clearer. This also applies to non-obvious variables and anything else you leave for the compiler to infer, too.

It is true that editors will be able to show you this information either way, but I think it is still nice to have explicit types so the code its self is readable regardless of tooling.

Well-defined types are lovely

You’ll soon realise how nice well-defined types are to work with. It is most definitely worth the initial pain of forcing yourself to define everything.

I often see objects in JavaScript and wonder, “alright, but what is it?”, when I see it being dotted into. It is such a pleasant developer experience when everything has a definition you can easily read through and understand.

This, combined with good editor support will leave a smile on your face:

Tab-completion

As you can see, without is just terrible.

You can contribute third-party types

Most of us have reached a point where we try to import a third-party dependency and it has no types, so we’re either left with a nasty compile error or we were naughty and turned the most lenient compile options on…

We’re all in need of good, strong types for popular dependencies. So if you do find yourself in such a situation, please do write them and contribute them to DefinitelyTyped or the project its self!

It will be of great help to all people who reach the same problems you have in future.

You will learn more advanced types

As you go deeper into the type system and start defining much more complex objects, you’ll learn a lot of fun things.

Things you once thought were too dynamic or difficult to strongly type will end up being easy for you.

A few good examples of what we can do are:

Wrap up

So to wrap up:

{
  "compilerOptions": {
    "strict": true,
    "noUnusedParameters": true,
    "noUnusedLocals": true,
    "noImplicitReturns": true
  }
}

Then, if you’re ready for it, enable ESLint with the no-explicit-any rule at error level.

Do this, take the hit and you will thank yourself later :)