DEV Community

Tim Raderschad
Tim Raderschad

Posted on • Updated on • Originally published at cstrnt.dev

3 TypeScript Tricks I wish I knew when I learned TypeScript

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
})
Enter fullscreen mode Exit fullscreen mode

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);
})
Enter fullscreen mode Exit fullscreen mode

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);
})
Enter fullscreen mode Exit fullscreen mode

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);
  }
})
Enter fullscreen mode Exit fullscreen mode

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 using any whenever you can! It's better to use unknown 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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
anuraghazra profile image
Anurag Hazra

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> generic

Collapse
 
cstrnt profile image
Tim Raderschad

That's right! I just like the simplicity of only needing to memorize one keyword which then can be used with all types :)

Collapse
 
nutlope profile image
Hassan El Mghari

Great article Tim! Especially liked the last tip - that's gonna be my new default way to type objects!

Collapse
 
cstrnt profile image
Tim Raderschad

Thank you very much :) Really happy that it was useful to you :)

Collapse
 
eljayadobe profile image
Eljay-Adobe

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.)

Collapse
 
cstrnt profile image
Tim Raderschad

Yeah, most of those features are relatively new :D

Collapse
 
tanth1993 profile image
tanth1993

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

Collapse
 
cstrnt profile image
Tim Raderschad

You're welcome :)

Yeah I know that some big projects have a lot of anys in them, but I think it's up to us Devs to fix this ;)