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
Table of Contents
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.
(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 typeany
.
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 typeunknown
.unknown
is the type-safe counterpart ofany
. (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 😇
Latest comments (0)