TypeScript’s utility types can really change the way you structure your code once you learn them. It’s one of those things you can’t live without once you know about it.
Let’s learn about the most interesting ones, why used them and when
Partial
This utility type takes all the fields of an object and marks them as optional.
Say you have a type User as such for a form
type User {
name: string;
age: number;
birthday: date;
email: string;
}
We want the user to be able to start filling up the form but then be able to finish it later on.
It’s not really possible to user a type of User since we don’t have all the field and typescript would just complain. That’s a perfect usecase for the Partial type. It takes all the field of the object and marks them as optional allowing us to pass an incomplete object to the function.
function saveUserFormData(User: Partial<User>)
/*
This is the type that the function above accepts
{
name?: string;
age?: number;
birthday?: date;
email?: string;
}
*/
Another great usecase is for mocking object, instead of having to create an object with fields we might not need for out test, we can mark all the fields as optional and create an object with only the ones we need
Pick
Now say we have a function that will get a specific user using it’s id You might want to do something like this
getUserById(id: string): User
while that works, if you ever decide to rename id to something else or change it’s type TypeScript isn’t going to help you out in this situation. A better way is to use Pick which let’s you pick one or multiple fields from your type
getUserById(id: Pick<User, "id">): User
If we ever decide to rename it to userId for some reason, TypeScript will let us know right away!
Exclude
Say we have a game where we have 3 states, Idle, Shuffling and Assigning. Now we need to create a function to handle each state but, we only really need to do something in the last two states. We can use Exclude here to make sure that the function should not be passed Idle.
type State = "Idle" | "Shuffling" | "Assigning"
function changeState(state: Exclude<State, "Idle">) {
switch (state) {
case: Shuffling:
...
case: Assigning:
...
}
}
Our function is way cleaner and we don’t need to have a useless state.
Small bonus
Here’s a small bonus that build on the previous example. When we use typescript, we have to choose between runtime safety aka having a default case which will catch all the types the possible states not defined in the switch case but, that means that TypeScript can’t tell us when we forgot one of states
type State = "Idle" | "Shuffling" | "Assigning" | "End" // New End state
function changeState(state: Exclude<State, "Idle">) {
switch (state) {
case: Shuffling:
...
case: Assigning:
...
default:
throw new Error("state not found")
}
}
// case End not handled
The other option is to have build time safety and not include a default case. TypeScript will then tell us that we’re missing one state that isn’t handled but, wh lose the runtime safety.
The solution is to use satisfies never which let’s use have the best of both world.
type State = "Idle" | "Shuffling" | "Assigning" | "End"
function changeState(state: Exclude<State, "Idle">) {
switch (state) {
case: Shuffling:
...
case: Assigning:
...
default:
throw new Error(`state ${state satisfies never} not found`)
// TypeScript errors here saying there's a state not handled
}
}
Never represents a type that isn’t assignable and so, at each step TypeScript narrows down the type of state.
After we handle the Shuffling case, it knows that state can now only be Assigning | End
Once we handle the case Assigning, the type is narrowed down to End only which does not satifie the type never so TypeScript errors telling you there’s a unhandled case.
Pretty neat!
There are a bunch more utility types I didn’t cover this article was just to show a small glance of how they can be used and what they might be useful for. I really encourage you to do more research on them as they can make your code so much cleaner.