Logo

TypeScript Utility Types Part 3: Extract, Exclude, and NonNullable

5 min read · May 25, 2020

Category: Web DevelopmentTagged with: TypeScriptTypeScript Generics
intersecting-types

TypeScript utility types provide built in type composition tools to generate new types. They capitalize on TypeScript generic types to enable this functionality. In the third part of this series, we will be covering the Extract, Exclude, and NonNullable utilities. For more coverage on other utility types, check out the previous two posts in the series.

Extract

Extract<T, U>. is a utility for pulling out values that are shared between the two type arguments it receives. This can be useful for refining types for other implementations, or to remove types that another implementation may not accept.

interface UserBase {
  email: string
  image: string | null
  username: string
}

interface UserProfile {
  id: string
  email: string
  image: string | null
  isAdmin: boolean
  username: string
  reviews: string[]
}

/*
 * If we want to find the shared keys between these interfaces, we can combine
 * keyof with Extract. keyof will give us a union string of all the keys, and
 * Extract will return the shared values.
 */

type SharedUserKeys = Extract<keyof UserBase, keyof UserProfile>
// 'email' | 'image' | 'username'

By using Extract on the two user interfaces, we have a type of the three shared properties. This type can then be used to check that a given argument for a function is one of these keys and not one that is either unknown or not shared. If we wanted to transform this back into an object we can combine Extract with our own custom type.

interface UserBase {
  email: string
  image: string | null
  username: string
}

interface UserProfile {
  id: string
  email: string
  image: string | null
  isAdmin: boolean
  username: string
  reviews: string[]
}

/*
 * If we want to find the shared keys between these interfaces, we can combine
 * keyof with Extract. keyof will give us a union string of all the keys, and
 * Extract will return the shared values.
 */

type SharedUserKeys = Extract<keyof UserBase, keyof UserProfile>
// 'email' | 'image' | 'username'

/*
 * Convert our union string back into an object type with the shared
 * properties and their corresponding value types.
 */
type SharedUserData = {
  [K in SharedUserKeys]: UserProfile[K]
}

const user1: SharedUserData = {
  email: 'test@example.com',
  image: null,
  username: 'sampleuser',
}

/*
 * Here we can eliminate the SharedUserKeys intermediary step, and return
 * a new object type with the shared properties between two other types
 */
type IntersectingTypes<T, U> = {
  [K in Extract<keyof T, keyof U>]: T[K]
}

const user2: IntersectingTypes<UserBase, UserProfile> = {
  email: 'test@example.com',
  image: null,
  username: 'sampleuser',
}

In both the SharedUserData and IntersectingTypes types, we utilize the [K in OTHER_TYPE] pattern. This allows us to iterate over a union type, in this case 'email' | 'image' | 'username' and set the key name as the current iterator. In both cases we set the value for each key by referencing the parent type. This works similar to referencing a value on a JavaScript object with a dynamic key. The IntersectingTypes implementation works in the same way, but inlines the usage of Extract that we previously had in a separately declared type. Here we also create a type with our own generic types, T and U, that will represent our base objects.

Exclude

Exclude<T,U> works similarly to Extract but performs the inverse operation. It will return all of the values present in T that are not present in U. In our previous example, we can use this to find all of the properties that are unique to UserProfile.

interface UserBase {
  email: string
  image: string | null
  username: string
}

interface UserProfile {
  id: string
  email: string
  image: string | null
  isAdmin: boolean
  username: string
  reviews: string[]
}

type ProfileSpecificKeys = Exclude<keyof UserProfile, keyof UserBase>
// 'id' | 'isAdmin' | 'reviews'

type ExcludedTypes<T, U> = {
  [K in Exclude<keyof T, keyof U>]: T[K]
}

const user: ExcludedTypes<UserProfile, UserBase> = {
  id: '1234',
  isAdmin: false,
  reviews: [],
}

NonNullable

NonNullable<T> will take a type and remove null and undefined from the allowed types. This can be useful for instances where a type may allow for nullable values farther up a chain, but later functions have been guarded and cannot be called with null. For example an application may have the concept of a nullable user at the top level of the application. Here we should be guarding against null values. We may have several internal functions that always expect a user to be present. We could use a higher order function to address the null guard before calling these functions. In that case, we no longer need the null branch for these internal functions and can remove them with NonNullable.

interface UserBase {
  email: string
  image: string | null
  username: string
}

// NullableUserBase can be a UserBase or null
type NullableUserBase = UserBase | null

const missingUser: NullableUserBase = null

// This will throw a compilation error as null has been removed
const requiredUser: NonNullable<NullableUserBase> = null

It is important to note that this utility is not recursive. In the case of our user example, image can still be null. If we wanted to change the type of image to disallow null, we would need to extend the base type and override specific properties.

interface UserBase {
  email: string
  image: string | null
  username: string
}

interface RequiredImage extends UserBase {
  image: NonNullable<UserBase['image']>
}

const user: RequiredImage = {
  email: 'hello@example.com',
  image: 'my/image/url',
  username: 'sampleuser',
}

TLDR

Today we covered three new TypeScript utilities:

  • Use Extract when we want to pull out a set of given types from a parent type.
  • Use Exclude when we want to eliminate a set of given types from a parent type.
  • Use NonNullable when we want to remove null and undefined from a a type. This is not a recursive operation.

As with other utility types we have covered in previous posts, these can help us to keep reuse and shape existing type definitions throughout our application. What TypeScript utilities have you found most valuable? What questions do you have about using TypeScript utilities or generics? Let's continue the conversation on Twitter.

Related Posts

TypeScript Utility Types Part 1: Partial, Pick, and Omit

TypeScript provides multiple means of creating, modifying, and extending existing types into new variants using special utility types. Most of these types utilize generic types under the hood, but a… Read more

TypeScript Utility Types Part 2: Record, Readonly, & Required

TypeScript utility types provide built in type composition tools to generate new types. They capitalize on TypeScript generic types to enable this functionality. Previously we talked about the , , and… Read more

2022 Skyler Lemay. All Rights Reserved.