Tom Dohnal

State-of-the-art frontend development

Back to postsWatch video
November 14, 2021 โ€ข 7 min read

TypeScript ANY vs UNKNOWNโ€”A Deep Dive

In this blog post, we'll dive deep into what any and unknown types are, what are their similarities and differences, and when (not) to use them.

(You can find a video version of this article on YouTube! ๐Ÿ“บ)

TLDR;

// ANY
const anyValue: any = "whatever";

// OK in TypeScript (error in runtime!) ๐Ÿ‘‡
anyValue.do.something.stupid();

// UNKNOWN
const unknownValue: unknown = "whatever, too";

// Fails TypeScript check (prevents runtime error) ๐Ÿ‘‡
unknownValue.do.something.stupid();

Mnemonics to help you remember the difference ๐Ÿ‘‡

  • any -> first letter is an "A" -> Avoid TypeScript
  • unknown -> first letter is a "U" -> Use TypeScript

The any Type

The any type is something like an escape hatch from TypeScript.

What does that mean?

If your variable is of type any, you can: 1. Assign whatever you want to it ๐Ÿ‘‡

let anyValue: any

anyValue = 'hi, how is you? :)'
anyValue = { login: () => { alert('password?') } }

2. "Do" whatever you want with it ๐Ÿ‘‡

let anyValue: any = false

// OK in TypeScript but error in runtime
// ("...is not a function")
anyValue.toUpperCase()

// OK in TypeScript but error in runtime
// ("...Cannot read properties of undefined")
anyValue.messages.hey(':)')

Generally speaking, using any allows you to use variables without type checking.

soldier being shot by an arrow saying "any" (https://devrant.com/rants/3015646/i-dont-usually-post-memes-but-this-one-is-just-a-little-too-on-the-nose)

This means that you lose the main benefit TypeScript has to offerโ€“preventing runtime errors due to accessing non-existing properties.

You might now wonder why the heck would I even use any if it means giving up type checking altogether?

Generally speaking, you should strive to avoid it. To do that, I'd advise you to: 1) Use "strict": true in your tsconfig.json file to disable implicit any types

Implicit any means that if you don't annotate a variable or a function parameter in TypeScript, it'll be any by default. With "strict": true, the TypeScript compiler will throw an error if you've got an unannotated variable of type any.

2) Use the no-explicit-any rule in TypeScript ESLint. This will give you an ESLint warning whenever you use any.

However, there are some situations where any is helpful. We'll cover the main use cases in the final section in depth. Nonetheless, it can be useful when migrating JavaScript code to TypeScript or when dealing with untyped external libraries.

Careful! Some built-in TypeScript types use any When using functions like JSON.parse(...) or fetch(...).then(res => res.json()), the type of the result is any by default.

You can use something like JSON.parse(...) as { message: string } to give it a proper type. Nonetheless, it's useful to know about these as it's very easy to accidentally use any without even knowing about it.

You can use rules such as no-unsafe-assingment in TypeScript ESLint to get warnings in such scenarios.

Why does TypeScript behave in this not type-safe manner? Well, one of the possible explanations is that there was no other way to type these built-in JavaScript functions as there was no unknown type that would be better suited for this job. Let's have a look at what it does and how it differs from the any type.

The unknown Type

The unknown type was added to TypeScript in 2018 with its version 3.0 release. Its purpose was to provide a type-safe alternative to any.

From the official docs: TypeScript 3.0 introduces a new top type unknown. unknown is the type-safe counterpart of any. (https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type)

What does that mean? As the phrase "type-safe counterpart of any" suggests, the unknown type is similar to the any type in some way but different in others (namely, unlike any, it's type-safe).

Let's first examine the similarities.

You can assign whatever you want to an unknown type This works pretty much the same way as the any type ๐Ÿ‘‡

let unknownValue: unknown

unknownValue = 'hey, how\'re you doing? :)'
unknownValue = { signUp: () => { alert('email?') } }

With variables of type any you could do anything with such variables (call them, access random properties on them, etc.). This is not the case with unknown.

With unknown, TypeScript makes no assumptions about your variables.

let unknownVariable: unknown

// Error in TypeScript ๐Ÿ‘‡
unknownVariable.toUpperCase()

// Error in TypeScript ๐Ÿ‘‡
unknownVariable.how.are.you('today?')

// Error in TypeScript ๐Ÿ‘‡
const newVariable = unknownVariable + 10

This is very helpful as it prevents you from accidentally accessing non-existent properties, treating strings like functions etc.

You can think of the unknown type as of something like

type unknown = string | number | boolean | object | null | ...

Disclaimer: this is not the actual definition of the unknown type. It's just a simplified model to give you a better intuition.

Type narrowing with unknown

As we saw in the code snippet above, TypeScript doesn't allow you to do almost anything with the unknown type.

On one hand, it's very useful as it keeps your code type-safe and prevents you from the dreaded runtime JavaScript TypeErrors.

On the other hand, it's quite limiting as we'd like to be able to manipulate our variables, call properties on them, etc.

We've got 2 options for how to do that.

1) Type Guards We can use type guards to narrow down the possible types. You can use if statements or custom type predicates to do that.

function logSecretMessage(message: unknown) {
  if (typeof message === 'string') {
    // in this if-block we know `message` is of type string
    // so we can call the `toLowerCase()` method on it
    console.log(message.toLowerCase())
  } else {
    console.log(message)
  }
}

2) Type Assertions (not type-safe) Alternatively, we can always use type assertions. This is way easier but we lose the type-safety as we use whatever type we want and TypeScript will just "trust" us that we made no mistake:

const unknownVariable: unknown = 'hello';

// OK ๐Ÿ‘‡
(unknownVariable as string).toUpperCase();

// OK in TypeScript but it *fails* in runtime ๐Ÿ‘‡
(unknownVariable as number).toFixed(2)

Use Cases for any and unknown Types

Now that we've got understanding of what the any and unknown types mean, let's have a look at when (not) to use each of them.

The rule of thumb is that any should be avoided since using it makes you lose most of the TypeScript benefits. If you don't know what type a certain variable or a function parameter is, always prefer unknown.

With that being said, there are some valid use cases for any.

Migrating to TypeScript (from JavaScript)

any is very useful when migrating JavaScript codebase into TypeScript.

Let's say you've got a large JavaScript file which exports many functions and you want to convert it to TypeScript. Without using any, you'd need to type every single function in this file.

That's a lot of work and you might just be interested in typing one of the exported functions.

In this case, you can use any to quickly type the functions you're not interested in and only give proper types to the one function you're currently working with.

It might be also useful to create an alias for any (such as type TODO = any) so that you can later come back to your temporarily typed functions and give them proper types.

// auth.ts (migrating from auth.js)
type TODO = any

// We can just type the `signUp` function
export function signUp(options: {
  email: string
  password: string
}) { /* ... */ }

// Use `TODO` to quickly type the `resetPassword` function
export function resetPassword(options: TODO) { /* ... */ }

// Use `TODO` to quickly type the `logIn` function
export function logIn(options: TODO) { /* ... */ }

Functions with unknown arguments

As previously stated, the unknown type should be preferred when dealing with variables which types we can't determine.

An example could be a generic logger function ๐Ÿ‘‡

function logger(message: unknown) {
  if (development) {
    console.log(message)
  } else {
    sendMessageToAPI(message)
  }
}

Had we used any instead, we could have accidentally tried to use properties such as .toLowerCase() (wrongly) assuming that message is of type string.

Thus, using unknown instead keeps things safe for us ๐Ÿ˜‡

Subscribe to the newsletter

Stay up to date with state-of-the-art frontend development.

No spam. Unsubscribe any time.

hey@tomdohnal.com