When implementing TypeScript in our projects, we strive to write the best typings we can. We might often feel like using the any type defeats the purpose of TypeScript, and rightfully so. There are also some other types worth knowing, and we might find them useful when trying not to use any, like the unknown. In this article, we also discuss never and void.
Any
The any type resembles how working with a pure JavaScript would be like. We sometimes might need to describe a variable that we don’t know the type of at all.
1 2 3 |
let uncertain: any = 'Hello world'!; uncertain = 5; uncertain = { hello: () => 'Hello world!' }; |
In TypeScript, everything is assignable to any. It is often referred to as a top type.
Writing the code in such a manner does not seem proper. It is unpredictable and hard to maintain. You might feel the need to use it when dealing with some third-party libraries that have no typings created for them, and you are not sure how they work. Also, using any might be a way to add TypeScript to your existing JavaScript codebase.
By using any, we undermine the ability of TypeScript to prevent us from causing trouble. There is no type-checking enforced, and therefore it might give you some headaches.
1 2 |
const uncertain: any = 'Hello world!'; uncertain.hello(); |
TypeError: uncertain.hello is not a function
And there you go, an error ready to ship to production! The above example is very vivid, but it could be more subtle.
1 2 3 4 5 6 |
const dog: any = { name: 'Fluffy', sayHello: () => 'woof woof' }; dog.hello(); |
TypeError: uncertain.hello is not a function
It would be beneficial to figure out some more detailed typings.
Unknown
The unknown type introduced in TypeScript 3.0 is also considered a top type, but a one that is more type-safe. All types are assignable to unknown, just as with any.
1 2 3 |
let uncertain: unknown = 'Hello'!; uncertain = 12; uncertain = { hello: () => 'Hello!' }; |
We can assign a variable of the unknown type only to any, and the unknown type.
1 2 |
let uncertain: unknown = 'Hello'!; let notSure: any = uncertain; |
It does differ from any in more ways. We can’t perform any operations on the unknown type without narrowing the type.
1 2 |
const dog: unknown = getDog(); dog.hello(); |
Unable to compile TypeScript:
Property ‘hello’ does not exist on type ‘unknown’.
Narrowing down the unknown with type assertions
The above mechanism is very preventive but limits us excessively. To perform some operations on the unknown type, we first need to narrow it, for example, with a type assertion.
1 |
type getDogName = () => unknown; |
1 2 |
const dogName = getDogName(); console.log((dogName as string).toLowerCase()); |
It the code above, we force the TypeScript compiler to trust that we know what we are doing.
A significant disadvantage of the above is that it is only an assumption. It has no run-time effect and does not prevent us from causing errors when done carelessly.
1 2 |
const number: unknown = 15; (number as string).toLowerCase(); |
TypeError: number.toLowerCase is not a function
The TypeScript compiler receives an assumption that our number is a string, and therefore it does not oppose treating it as such.
Using control-flow based narrowing
A more type-safe way of narrowing down the unknown type is to use a control-flow narrowing.
The TypeScript compiler analyses our code and can figure out a narrower type.
1 2 3 4 5 |
const dogName = getDogName(); if (typeof dogName === 'string') { console.log(dogName.toLowerCase()); } |
In the code above, we check the type of the dogName variable in the run-time. Therefore, we can be sure that we call the toLowerCase function only if the dogName is a variable.
Aside from using typeof, we can also make use of instanceof to narrow the type of a variable.
1 |
type getAnimal = () => unknown; |
1 2 3 4 5 |
const dog = getAnimal(); if (dog instanceof Dog) { console.log(dog.name.toLowerCase()); } |
In the code above, we make sure that we call dog.name.toLowerCase only if our variable is an instance of a certain prototype. TypeScript compiler understands that and assumes the type.
If you want to know more about prototypes, check out Prototype. The big bro behind ES6 class
There is currently an interesting suggestion stating that TypeScript should also use the in operator when narrowing types to assert property existence. If you’re interested, check out this issue in the TypeScript repository.
A workaround provided by Tom Crockett is to use a custom hasKey type guard.
1 2 3 4 5 |
const dog = getAnimal(); if (typeof dog === 'object' && hasKey('name', dog)) { console.log(dog.name); } |
Differences between Void and Never
The void and never types are often used as return types of functions. To avoid confusion, let’s compare them.
Void
The void type acts as having no type at all.
1 2 3 |
function sayHello(): void { console.log('Hello world!'); } |
It is useful to indicate that we are not supposed to use the return value of the above function. We can spot the difference between the void and undefined here:
The void type is common in other languages like C++, where it serves a similar purpose.
Never
The never type indicates that a function never returns. We sometimes refer to it as the bottom type.
A typical example is when a function always throws an error:
1 2 3 |
function throwUserNotFoundError(userId: number): never { throw new Error(`User with id ${userId} is not found`); } |
Another example is when the function has a loop that never ends.
1 2 3 4 5 |
function renderGame(game: Game): never { while (true) { game.renderFrame(); } } |
Also, the TypeScript compiler asserts the never type if we create an impossible type guard:
1 2 3 4 5 |
const uncertain: unknown = 'Hello world!'; if (typeof uncertain === 'number' && typeof uncertain === 'string') { console.log(uncertain.toLowerCase()); } |
Property ‘toLowerCase’ does not exist on type ‘never’.
Summing up both never and void:
- A function that does not return any value explicitly has a return value of undefined. To indicate that we ignore it, we use the void return type
- A function that never returns at all due to some reasons has a return type of never
Summary
In this article, we’ve gone through the differences between any and unknown. A conclusion from comparing the above is that the unknown type is a lot safer because it forces us to do additional type-checking to perform operations on the variable. We’ve also gone through the never and void types. By doing so, we differentiate functions that don’t return values from the ones that don’t return at all.
It was a helpfull post. Thank you.