Massaging types in typescript (part 2)

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 type T. So given type User = { name: string, address: Address | string } and type UserAddress = Pick<User, 'address'> results in type 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 and type UserName = Omit<User, 'address'> results in type 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 }. So type StrictAddress = Exclude<Address, { city: string }> would result in type 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.


In case you've missed it, here's the link to part 1.

Ping me on twitter. 🤘

Comments

Popular posts from this blog