TLDR;
In part 1 we've covered three of the most basic type utils that TypeScript provides: "typeof", "keyof" and "indexed access".
In type two we'll learn about:
- Pick 👌 - constructs a new type by extracting the
Keys
out of typeT
. So giventype User = { name: string, address: Address | string }
andtype UserAddress = Pick<User, 'address'>
results intype UserAddress = { address: Address | string }
. - Omit 📌 - the exact opposite of Pick. It picks all type properties by default, but omits the
Keys
. So having the above example andtype UserName = Omit<User, 'address'>
results intype UserName = { name: string }
. - Exclude 🪚 - a handy way to "strip down" a union type. Let's introduce the type
type Address = { lat: string, lon: string } | { city: string }
. Sotype StrictAddress = Exclude<Address, { city: string }>
would result intype StrictAddress = { lat: string, lon: string }
.
If you need a deeper take on this, continue reading.
It's really common to want to reuse a type, but to cherry-pick some of it's properties. Pick to the rescue!
Let's keep using the example with an imaginary User.
type User = {
name: string,
address: { lat: string, lon: string } | { city: string }
}
We also have a shipping service, which expects as an argument a user with a valid address.
function shippingService = (user: ❓) { ... }
Now, what do we replace the question mark with above?
Option one is to copy-paste the address type and create our user by hand.
function shippingService = (user: {
address: { lat: string, lon: string } | { city: string }
}) { ... }
In general, that sucks. Don't do that. ❌
Option two is to alias the address type:
type Address = { lat: string, lon: string } | { city: string };
function shippingService = (user: { address: Address }) { ... }
That's ok-ish. ✅
But there's a third option. To use Pick:
function shippingService = (user: Pick<User, 'address'>) { ... }
Great. ✅
Pick<User, 'address'>
gives us a user object with only the address of the user:
type ShippingUserData = {
address: { lat: string, lon: string } | { city: string }
}
The benefit of the third approach is that we're not introducing a new type, increasing the type surface of our app. Plus, if we decide that the user is going to have another address representation, we define it there only once and that's it.
Massaging types at its best 🔥.
The Omit<T, Keys>
Now you know about Pick. The Omit does the exact opposite of what Pick does.
We can define the shippingService
like this:
function shippingService = (user: Omit<User, 'name'>) { ... }
where the user argument's type resolves to:
type ShippingUserData = {
address: { lat: string, lon: string } | { city: string }
}
The resulting type of Omit<User, 'name'>
is exactly the same as Pick<User, 'name'>
in our example. But there's one catch, tho! ⚠️
Omit<T, Keys>
, by default, leaves all of the T
's properties, but the Keys
that you provide to it. Did you spot the problem in our example above?
Now our user has only two properties - name
and address
. Regardless of whether we use Pick
or Omit
, we can get a type that consists of only one of the two properties.
Now, if we introduce a third one, say fortune: number
, here's what happens.
type UserWithoutAddress = Omit<User, 'address'>; // { name: string, fortune: number } -> get everything, except 'address'
type UserWithOnlyName = Pick<User, 'name'> // { address: { lat: string, lon: string } | { city: string } -> get only the 'name'
So, the moral of the story is, use Pick
, when you want to cherry-pick specific type properties and Omit
, when you want to get all type properties, except a specific one or two.
The Exclude<T, U>
That's an interesting one.
Here we have an UserWithAddressOnly
type.
type UserWithAddressOnly = {
address: { lat: string, lon: string } | { city: string }
}
That's great, but at some parts of our app, we need a strict lat/lon
address, not an arbitrary city
string.
That's perfect candidate for Exclude<T, U>
.
So:
function showUserOnTheMap(user: ❓) { ... }
How do we replace the question mark, so that we get the following type: { address: { lat: string, lon: string } }
?
Sure, we can define the type on the fly, but we have it already defined in Address
. Let's reuse it.
type StrictAddress = Exclude<Address, { city: string }>; // results in { address: { lat: string, lon: string } }
Such a beauty. Now, we can safely use our user.address
prop in the showUserOnMap
function.
Photo by Jocelyn Morales on Unsplash
In case you've missed it, here's the link to part 1.
Ping me on twitter. 🤘