Number 1: Readonly<T>
Let’s start with a small example:
We have a simple function which takes in an array of numbers and returns an array with all elements sorted.
function sortNumbers(array: Array<number>) {
return array.sort((a, b) => a - b)
}
Now look at the code below and look if everything looks good. Think about what the console output will be. I recommend taking some time and actually thinking about it!
const numbers = [7, 3, 5];
const sortedNumbers = sortNumbers(numbers);
console.log(sortedNumbers);
console.log(numbers)
The first output is pretty simple. It is [3, 5, 7]
. But now listen. The second output is the same! And you might be asking: Why? I defined the array as const
how can it be changed?.
Well, arrays and objects are quite special in JavaScript. If you pass them to a function it will pass the reference to the array or object which means it will mutate the original array if you call certain functions like Array.sort
which are in-place.
Readonly to the rescue 🚀
Let’s change up our code a little bit:
function sortNumbers(array: Readonly<Array<number>>) {
return array.sort((a, b) => a - b)
}
This doesn’t compile though. TypeScript gives us the following error Property ‘sort’ does not exist on type ‘readonly number[]’
Which is actually what we want! We are not able to mutate the parameter which leads to zero side effects!
Nice.
But does this mean we can’t have function which sorts our arrays? Of course we can. We only need to sort a copy of our array rather than sorting the array itself. There are many ways to copy an array in JS like spreading it ([…array]
), using array.concat()
, Array.from(array)
or array.slice()
. So let's use the spread operator to finish our function so it looks just like this
function sortNumbers(array: Readonly<Array<number>>) {
return [...array].sort((a, b) => a - b)
}
And we’re done! Clean code enforced by TypeScript. BTW: This also works with objects!
If you want to learn more about mutability in JS check out this article
Number 2: Any vs Unknown
When you are using eslint together with TS you might have noticed the message unexpected any
. At least I was wondering why any
is bad. How else should you state a variable can hold any possible value. Let’s look at an example here:
const someArray: Array<any> = [];
// add some values
someArray.push(1);
someArray.push("Hello");
someArray.push({ age: 42 });
someArray.push(null);
We are creating an array that can potentially have all available types in it. While this might not be the best code ever, let’s just go with it. We add a number, a string and an object. Let’s now look at the code below and think about what will happen:
const someArray: Array<any> = [];
// ... adding the values
someArray.forEach(entry => {
console.log(entry.age);
})
This code is actually valid TypeScript and will compile without any issues. But it will fail at run time. Why? Because as soon as we loop over an entry which is null
or undefined
, and then try to access .age
, it will throw an error like this:
Uncaught TypeError: Cannot read properties of null
.
I think this is some kind of false security because you expect things to just work. After all the TS compiler told you the code is fine.
But we can fix this! And the change is actually super simple. Instead of typing the array as Array<any>
we can just use Array<unknown>
if we now use the same code but with that change it will look like this
const someArray: Array<unknown> = [];
// ... adding the values
someArray.forEach(entry => {
console.log(entry.age);
})
and this code will not compile! Instead, TypeScript shows the following error when we try to access entry.age
// ... other code
someArray.forEach(entry => {
// Object is of type 'unknown'
console.log(entry.age);
})
using unknown
enforces us to check the type (or explicitly casting the value) before we do something with a value with is unknown
. Let's look at an example:
// ... other code
type Human = { name: string, age: number };
someArray.forEach(entry => {
// if it's an object, we know it's a Human
if(typeof entry === 'object'){
console.log((entry as Human).age);
}
})
In this case we checked whether the value is an object and then access the .age
property. And because this is such a abstract topic, here is a little wrap-up:
any
is basically saying the TypeScript compiler to not check that bit of code. Avoid usingany
whenever you can! It's better to useunknown
instead because it enforces you to check the type of the value before using it or else it won't compile!
Note: don't use typeof x === 'object'
to check whether something is a valid object, because it will return true
for arrays as well.
Number 3: Typing Objects with Records
When I first started using TS I always had to google how to type an object because I could never remember the solution which looked something like this:
interface Person {
[key: string]: unknown
}
const Human: Person = {
name: "Steve",
age: 42
}
While this is a valid solution to type an Object in TS, I think it’s pretty hard to memorize and also it is pretty limited.
For example if I only want to allow certain keys I would go ahead and create a string union like this:
type AllowedKeys = 'name' | 'age';
interface Person {
[key: AllowedKeys]: unknown
}
const Human: Person = {
name: "Steve",
age: 42
}
But, TypeScript doesn’t like this and gives me that error:
An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
Uhm, what? This is again one of those TypeScript errors which wants you to just close your IDE and go back to plain JS. But there is a solution which will make the code much more readable:
type AllowedKeys = 'name' | 'age';
// use a type here instead of interface
type Person = Record<AllowedKeys, unknown>;
const Human: Person = {
name: "Steve",
age: 42
}
We only had to change from interface to type so we can define a new type and then use the keyword Record
which takes two generic parameters where the first one is the type of the keys and the second on of the according values. Pretty simple, right? And by the way, if you now add values to AllowedKeys
it will throw an error in the Human
Object because it’s missing those properties which is pretty awesome if you ask me!
This post was originally published on cstrnt.dev
Top comments (8)
In the first example, you don't need to use two generics to define readonly arrays.
You can simply use the built-in
ReadonlyArray<number>
genericThat's right! I just like the simplicity of only needing to memorize one keyword which then can be used with all types :)
Great article Tim! Especially liked the last tip - that's gonna be my new default way to type objects!
Thank you very much :) Really happy that it was useful to you :)
I wish I had known all this when I was programming in TypeScript.
(But I suspect TypeScript 0.8 didn't have any of these features yet.)
Yeah, most of those features are relatively new :D
nice. Thanks for sharing
in real project, there are some cases cause the data null or undefined, so I get used to using any
I have read Record Utility Types and now I know how to use it correctly
You're welcome :)
Yeah I know that some big projects have a lot of
any
s in them, but I think it's up to us Devs to fix this ;)