TypeScript: Stop Using 'any', There's a Type For That

When we work with any(😬) amount of TypeScript code, chances are we will encounter the any keyword. Most of the uses we have seen indicate that we are dealing with the base type in TypeScript land. Ruby’s is Object same for C#, inside the documentation we find:

(…) values from code that has been written without TypeScript or a 3rd party library. In these cases, we might want to opt-out of type checking. To do so, we label these values with the any type:

What is any

So it is not a wildcard, and it is not the base type; it is explicitly to interact with 3rd party libraries. Then why is it around so often? Is it detrimental to our systems? Should we run from it or embrace it?

The any type is a powerful way to work with existing JavaScript, allowing you to gradually opt-in and opt-out of type checking during compilation.

Let that sink in. The TypeScript documentation express clearly that when we have the any type, we are telling the compiler:

nothing to see here

We are saying no thank you when over 500 contributors to the language offer their help. It sounds like to opt-out of the type checker, and with it, losing all security and confidence in our type system should not be a decision taken lightly. We should be using it to interact with non-typed 3rd party(or 1st party) Javascript code, or when we only know some part of the type.

But wait I have a lot of other reasons

Isn’t TypeScript transpiling to Javascript? Isn’t Javascript dynamic? Why should I take care of my types then?

Yes! But we are writing our code in TypeScript, which is a statically typed language. One can argue that statically typed languages don’t result in fewer bugs than dynamic languages. Still, in a statically typed language where we use something like any, that’s the worst of both worlds.

Some things are hard to type correctly, and any is easier

It is easy to indulge the lazy developer in us. If we don’t type it correctly, we’re going to write bugs, more bugs than we would in a dynamic language because we force TypeScript, a statically typed language, to go without checking for incorrect types.

I really don’t know what it is

That’s fine! We can use unknown; it allows us to assign any type indeed. But we won’t be allowed to operate with the values until a specific type is determined.

type ParsedType = {
  id: number
}

const parseApiResponse(
  response: Record<string, unknown>
): ParsedType => {
  const convertedResponse = (response as ParsedType)

  // without doing the type cast we would
  // get a type error here
  if(convertedResponse.id >= 0) {
    return convertedResponse
  } else {
    throw Error.new("Invalid response"
  }
}

I have to write a lot of code when I add types, any is less work

It probably isn’t; if we are writing code without types, we will likely add defensive code to make sure arguments and variables have the correct shape for the program to perform as intended. any can’t even guard our logic against null or undefined checks.

// version 1 with `any`
const fullName = (user: any) => {
  if (user?.firstName && user?.lastName) {
    return `${user.lastName}, ${user.firstName}`
  }

  return user?.firstName || ""
}

// version 1 without `any`

interface User {
  firstName: string
  lastName?: string
}

const fullName = ({ firstName, lastName }: User) => {
  if (lastName === undefined) {
    return firstName
  }

  return `${lastName}, ${firstName}`;
}

Types add so much complexity, sometimes any is simpler

Using any might allow us to make progress without putting too much thought into how the data flows into our logic. But it shifts that burden to future readers of our code. They will have to interpret what is happening without the context we had and without help from the compiler.

With documentation I can provide all the context

When we add the types, we are getting help from the compiler, and we are getting documentation that will not decay with time, because our code won’t compile if it is outdated.

const intersection = (a: any, b: any): any => {
...
}
const intersection = (
  a: Set<number>, b: Set<number>
): Set<number> => {
...
}

They are both equivalent, but the reader will have a better idea of what the last function is doing, not so much from the first one.

But I’ve written the code in a defensive way with the necessary runtime checks to ensure there isn’t an error

There might not be an error now, but unless you have excellent test coverage, someone coming in to change that code later can’t have confidence that they’re not refactoring in a bug; it’s almost like the compiler won’t help you because we said it not to. If we explicitly set the types and change an API consumed in our system, the compiler will offer its guide.

What if I later change my mind about the type? Having it all explicit will have me refactoring for hours.

We can always modify and accommodate new type definitions. TypeScript offers an array of utilities for this purpose. We can use Pick to, well, pick the properties we need from a previously defined type. Omit to get everything but a handful of them. Partial to make all attributes optional or do a full 180 and make them all Required.

type User = {
  id: number;
  firstName: string;
  lastName: string;
  age: number;
}

type UserParams =
  Pick<User, "id"> & Partial<Omit<User, "id">>

const updateUser = (
  { id, ...newUserParams }: UserParams
) => {
  {...}
}

Fine, deleting any from TypeScript, opening a PR right now

Let’s take a deep breath, any is extremely powerful and useful in the right scenario.

  • Interfacing with libraries that use it; make sure we turn it into the right type as soon as possible before moving that data through the system.

  • Getting around TypeScript type bugs; if we find ourselves in a situation where it’s impossible to type something, any might be necessary. But only resort to this after trying every other approach. And if we use it, we should turn it back into a predictable type ASAP.

  • If our function can genuinely handle any type, this is rare and situational (such as a debug or logging function maybe?) In these cases, we need to be 100% sure there is no type in existence that will cause the function to fail. We should to examine the body of the function and determine the most basic shape from the input and restrict it. For example, if we want to print something, we should, at a minimum, verify that it responds to toString. Small steps.

Let’s recap:

Why shouldn’t we use any again?

  • It yields the compiler obsolete:
    • We’re telling the compiler Help not needed, I got this
  • We’re passing on an opportunity to document the code as we write it
  • Our first line of defense is down
  • In a dynamic language, we assume that things can have any type, and the patterns we employ follow this assumption. If we start using a statically typed language as a dynamic one, then we’re fighting the paradigm.
  • As we continue to make changes to the codebase, there’s nothing to guide/help us.
  • With great freedom comes great responsibility (the compilers). Don’t turn into a compiler, use one.