Development
Welcome to part two of the article, I hope you are ready for more coding tips. Today I will show you how to make types change depending on some conditional types and explain how to work with type unions to leverage things you can do with them.
Conditional types allow you, as their name implies, to conditionally determine which type should something be.
I will get to the example in just a bit, but let me first introduce you to the never type in Typescript. The never type is simple to explain: use it for situations you never want to happen - that’s it. So, the code I will show you creates a generic type that will make your value always truthy. If the compiler cannot ensure that the value is truthy, it won’t compile.
As you can see, it is easy to write conditional types. The syntax is like a ternary operator in regular Javascript. So what can we do in this example? If T is a falsy value (zero, empty string, false, null, or undefined), the conditional type will result in a never. If it is not a falsy value, it will be whatever the T is.
So, if you try to run the code you will get a compilation error due to the second call of takesInOnlyTruthy. Because our Truthy type returns never if the value is falsey, such as undefined compiler will complain, you will probably get this message: Argument of type 'undefined' is not assignable to parameter of type 'never'. Just like I said, never is for situations you never want to happen. You never want to pass undefined if a function requires a Truthy.
You can use conditionals to determine the return type of a function. The only downside is that you must use “as” casting, which can make the code less safe, therefore my advice is to use this only on simple functions.
In the following example, you can see a method that will take only strings and numbers as input. If the string is provided, it will return a string. However, if a number is provided it will return undefined.
You can also use conditionals to tell what function arguments are supposed to be. Let me show you an example if you want to create a function that takes two arguments. If the first argument is a string, then the second argument must be a number, otherwise, the second argument must be a boolean:
So, you can define what function arguments should be depending on just one type, but what if we want a different number of arguments (or maybe even a different order)? You can do that using tuples. For those who don't know, they're arrays with known sizes and contents.
It works because in Javascript you can access function arguments via the arguments keyword:
Arguments are array-like objects, but most importantly, they implement an iterator. Therefore we can spread them directly inside a function and make an array out of them.
So how does this help us? As I said, tuples are like arrays, but they are arrays under all, considering that Typescript is doing extra checks. In the end, once transpired to plain Javascript, they are just the same old arrays.
With more “spread” magic, we can get function arguments as we like. For example, let’s write a function called “print” that will take one argument called “input” which can be a string or number. I’ll name the second argument “casing” - it can be either “upperCase”, “lowerCase”, “default”. We will require a second argument only if the string is given for input.
Something like this:
It might look complicated but fear not, there is a simpler way to do this by using discriminating unions (it will be explained later in the article).
What does Watchout mean? It means that if you define an array like this:
distribution converts them to this:
Why can this be dangerous? Look at this code sample:
Do you see the issue? No? Okay, let me help you. The goal here was to make sure that if the Foo type is given, we want to have the caller property. In case we pass in Bar, we don’t want it. In this example, we have the wrong property - we passed the Bar type, yet Typescript allows us to define a caller. It can be dangerous; caller property could be called with wrong data or when not existing depending on how you set up your code. This all could happen because of type distribution. How can you overcome this problem? Use tuples to disable the distributive behavior of conditionals.
Remember how I earlier said that there is a simpler way to achieve different numbers (and types) of function arguments? Well, here it is. The idea is that you use Typescript unions to do that and the easiest way to understand it is by going back to the previous example. It was the one where we were building a function that prints input (which can be string or number) and requires a second argument which can be “upperCase”, “lowerCase” or “default” only if the input is a string. Take a look at this code:
It works because Typescript is smart enough to figure out that if the first argument is a string, we want to “take” the [string, 'upperCase' | 'lowerCase' | 'default'] as a type and if it is a number then we only want [number].
Of course, this approach is not limited only to function arguments, you can use it in other places too (Watchout: just make sure to wrap union in brackets after &):
Notice that sometimes when working with TSX (React) and using this in Props, you can come across a situation where the compiler would complain that not all fields are given (for example, requiring membershipType even though isFullMember is false). Doing so would give the error we encountered with the user2 of the above example. Solution? Make all properties present in both cases, but make them optional and set their type to never:
You can use unions to “write out” some of the types. Take a look at this situation:
We know that after filtering, the array will only have numbers, but Typescript does not. So how do we fix this? Well, we can use unions to do that. Let's just write the signature of the function:
We added a new function called removeFalsy. It takes a type T, but we defined input as an array of T | null | undefined | false | ‘’ | 0 and that is what I meant by we can “write out” some types. So, we said: “okay, I do not know what exactly T is, but I do not want it to be null | undefined | false | ‘’ | 0”. And by doing this we have “distanced” T from those other types/values. In the end, we need to say that our function returns the array of the only T by signing that with T[].
After that, we need to make this function work. Now, I’ll introduce you to the so-called type predicates and user-defined type guards. Simply said, it is a function that tells whether some input is some type.
Now let’s use it in our function. Here is the full code:
That’s it.
There is your gotcha moment. If you do this, the compilation won’t pass.
This happens because Typescript cannot ensure the type of array won’t change. We could also do this:
Since arr is initially false, the compiler considers that as a boolean. Therefore we can add true to the array. So in the end, removeFalsy would return an array of numbers and booleans. That is why Typescript does not allow that to compile.
Pro tip: you can use the as const keyword to “lock” the array. That makes the array completely unchangeable and works also for objects.
Doing this would ensure the compiler that the array will stay the same as it is. But you will need to adjust removeFalsy to work with read-only arrays too (there is no real difference in this case and no need for this, but you have to satisfy the compiler).
I hope you learned something new. If you are still new to Typescript, I hope you master it soon and then come back to this article. Maybe you even get an epiphany - “oh yes, that’s what that guy was talking about ''. And if you already know all of this, at least I refreshed your memory a bit. You know what they say, Repetitio est mater studiorum.
Mihael is a Frontend Web developer at COBE. When he’s not mastering his frontend skills in React and TypeScript, he's either reading books or watching movies.