TypeScript has some incredibly nifty utilities that can be used to make your codebase more readable, efficient and safer.
In this article, I've compiled a list of four of my favourite utilities that I use in my daily workflow, along with some examples and explanations of each.
They've helped my TypeScript workflow - I hope they help yours, too!
Before we get started...
If you're new to TypeScript, I have a full course for beginners available right here on my YouTube channel!
It covers all the essentials you need to get started with TypeScript as a JavaScript developer.
If that sounds like something you're looking for, check it out here - I'd love to hear your thoughts on it!
Pick and Omit
Pick
and Omit
are special utility types that TypeScript provides as a way to add more convenience and power when creating new types for object shapes. Let's take a look at each one in detail with some examples...
Pick
In the following example, we've constructed an interface
type called Consumable
, which has got a bunch of properties that relate to something you could eat or drink.
TypeScript provides the Pick
utility to allow us to "pluck" properties from our object shape types, and create a new type from that. Let's create a new type, Pizza
, by simply picking out the relevant properties from the Consumable
type.
Nice! Let's go over that in a little more detail.
- The first parameter that we pass into the
Pick
utility is the type that we want to pick from. - The second parameter is either a single value or a union type of all of the properties we want to pick out from the type we passed in as the first parameter.
In the above example, we're picking size
and caloriesPerServing
from the Consumable
type to construct our brand new type, Pizza
.
Let's go one step further. The cool thing about creating a new type is that we can use it just like anything else - so let's extrapolate our Pizza
type and add a toppings
property to our object shape...
In this example, we're declaring Pizza
as an interface
, so that we can extend from our new Pick
ed type and add a brand new parameter, toppings
, to it. That means that our Pizza
interface, after being compiled, would have the following properties:
- size: 'large' | 'medium' | 'small'
- caloriesPerServing: number
- toppings: string[]
Omit
Omit works just like Pick
- but the inverse.
We pass Pick
the properties we wish to pluck out from the object type, but with Omit
, we pass the properties we wish to exclude from the initial object type.
Let's take a look at an example to make things a little clearer. Just like with Pick
, we'll use the same Consumable
type once again as a base - but this time, we'll create a new type called Sandwich
.
Our Consumable
type has a property on it called millilitresPerServing
. That's not really relevant to a sandwich - so by using Omit
, we can pass in two arguments:
- First, the type that we wish to use as a base...
- ...followed by a single or union type of the keys that we wish to omit from that interface.
(Just like with Pick
!)
That means in this example, our Sandwich
type would have the following properties:
- size: 'large' | 'medium' | 'small'
- caloriesPerServing: number
- gramsPerServing: number
Notice that millilitresPerServing
isn't present in that list - that's because our Sandwich
type intentionally omits that from our new type by using the Omit
utility as described above.
What's just as cool - just like with Pick
, the previous example, we can use the new type generated by the Omit
utility as a base to extend from. Let's extend our Sandwich
type by adding some fillings
...
Omit and Pick really come into their own in more complex applications, particularly when you have a lot of overlapping object shapes that have properties which should remain identical in type. They're a dream for composition!
Required & Partial
Just like Pick
and Omit
that we covered above, Required
and Partial
are utility types that allow us to create new types from our object types. Let's take a look into each one to see how they could be used as part of a workflow.
Required
Okay, simple example - we have an interface for a (fictional) sign-up form on a website, with all the usual suspects present.
Notice that in the above example, we've got a few ?
s in there.
Those are use to indicate that those properties are optional - which means that they're allowed to be undefined
. Let's create an input object using our type:
(Note: I could have also just omitted all of of the properties with undefined
as a value, but I wanted this example to be a bit more explicit for easy reading!)
Let's say for example that we have another form in our web app elsewhere, which uses the same shape of input - but this time, requires that we supply values to all of the properties in our MyFormInputs
object.
If we wanted to, we could just re-write that same interface again, keeping all our keys and value types the same - but removing those pesky ?
s to ensure that we can't pass any undefined
values in...
...but, following the classic DRY rule, this should start to leave a bit of a bad taste in your mouth. There must be a better way...
Thankfully, that's where the wonderful Required
utility comes in!
Let's create a new type called MyFormInputsRequired
and make all of the properties on it non-nullable.
Required
simply takes one parameter - the interface or object type that we want to make all properties enforced. In the above example, we also create a new object using that interface, and ensure that every single property has a corresponding value.
If the key wasn't present in requiredInputs
, or if we supplied null
or undefined
as any of the values, this would throw an exception at compile-time.
Nice and safe!
Partial
Partial
is the exact opposite of Required
- instead of making all the properties in an interface or object type required, it makes them all optional. (if you've read this entire article from the top, you're probably beginning to notice a pattern...)
Let's take a look at an example on how it could be used. We'll go back to videogames to maintain some semblance of variation...
In the above example, we've introduced our VideoGame
interface, which has three properties on it which are all required.
Let's say we wanted to create a new type making all of the properties optional. We'll use the power of Partial
to make this happen...
In the example above, we create a new type named VideoGamePartial
, and, just like how we used Required
above, we pass the Partial
utility a single object type.
This creates a new type, copying the exact shape of the VideoGame
interface, but making all of the properties optional.
When we create a new object using our new VideoGamePartial
type (as demonstrated in the nintendoGame
value at the bottom of the above example), we can see that we're able to skip two of the previously required values - description
and ageRating
.
Taking this to an extreme, because Partial
makes all of our properties optional, it would actually be valid to use that type to simply create an empty object...
...but that's probably more of a hypothetical use-case, as I can't imagine that being super useful in day-to-day 😅
Finally, topping it all off (and attempting to drive home how cool these utilities are) - let's use our new Partial
type as a base to extend from!
In the above example, we create a new type called SonyVideoGame
, which extends from our VideoGame
type that has a set of properties which are all optional.
We've then added a new (required!) type to it called platform
. That means that all of the properties (and their respective optional states would be as follows):
- title: string - Optional
- description: string - Optional
- ageRating: '3+' | '10+' | '16+' - Optional
- platform: 'PS2' | 'PS3' | 'PS4' | 'PS5' - Required
Using composition and the power of TypeScript utilities, we've created a complex type which has a series of properties which are both optional & required. Neat, right?
Summary
And that concludes our whistle-stop tour on some of TypeScript's powerful utilities that are provided with the language. There's plenty of others that you can delve into over at the TypeScript handbook - but these four are some of my favourites.
If you're looking for more TypeScript learnings, I have a full video course on the basics of TypeScript over on my website at CodeSnap.io!
Happy TypeScript'ing!
Top comments (10)
when you
Pick<Consumable, "size" | "caloriesPerServing">
doesnt it mean that renaming those properties will raise runtime errors because ... well ... strings?Hey Vetras! It doesn't actually rename those properties - rather, it plucks them from the original interface and creates a new type consisting of the picked properties. So in that case, the resolved
Pizza
type would end up looking like:Hope that makes sense :)
sorry :) I might have explained myself wrong.
I meant; What if you rename the
size
property tocount
, for example.Would the Pizza be renamed to
pizza.count
?Can the IDE pick that up? (pun intended 😎)
I guess if you wanted to add
count
to the new type, could you do something like so:That way, you'd get size, caloriesPerServing and count as properties on the
Pizza
type?Unsure if that's what you're asking, but I hope that helps!
true. that is not what I meant. I mean renaming the original property named "size" to the new name "count" (as an example).
Refactoring is a big part of writing maintainable software and renaming is a basic corner stone of refactoring.
Hence my question.
I think I understand - so you're suggesting if we renamed
size
tocount
, this would cause issues?Type-checking would catch that (the beauty of TypeScript!). In the below example, we pick the two properties to create the new
Pizza
type...But if we renamed "size" to "count", as you suggested...
Then we would receive a type error at compile time on the
pepperoniPizza
declaration line, as the object's shape (Pizza
) does not have the propertycount
.We could then perform the changes we require on our type:
...which would resolve the issue.
Did that make sense? Hope that answers your question!
yes it does
yes it does
thanks!
Ah, that's great to hear. Sorry I misunderstood you initially! :)
Hi vetras,
TypeScript doesn't actually throw run-time errors because it is compiled (transpiled) to JS, and that code is the one used in production, and because JS is typeless, you can override and change values as you wish.
TypeScript will throw an error at the development stage because when the object's name is changed, it will not match the interface that was declared when the object was defined.
The IDE will highlight the error if configured correctly
Fascinating. Makes development really easy. Before 'Partial', I used to make separate interfaces for almost similar objects