State-of-the-art frontend development
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! ๐บ)
// 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 TypeScriptunknown -> first letter is a "U" -> Use TypeScriptany TypeThe 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
anyby 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-assingmentin 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.
unknown TypeThe 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.unknownis 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
unknowntype. It's just a simplified model to give you a better intuition.
unknownAs 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)
any and unknown TypesNow 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.
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) { /* ... */ }
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 ๐
Stay up to date with state-of-the-art frontend development.