Demystifying TypeScript's Extract Type
Many of the applications I develop at work utilize TypeScript for front-end
development to provide a high level of type safety while also remaining familiar
to developers who are used to JavaScript. In one such application, a need arose
that required a more sophisticated type than I use on a daily basis. After some
research, I discovered a solution using TypeScript’s Extract
type. In this
post, I’ll share a few examples of how you can use Extract
in your own
projects to improve type safety and developer experience.
Use case: Extracting subsets of a union
To get started with Extract
, let’s look at a simple example. Suppose we have a
type MixedArgs
, which is a union type containing four different members of
various types. Our goal is to “extract” the members of the union that are
functions, which we can do using Extract
. This would look something like this:
type MixedArgs = string | number | () => string | () => numbertype FunctionArgs = Extract<MixedArgs, Function>
The first argument we pass to Extract is our union type and the second argument is the type that we will use when comparing each member of the union. If a member is assignable to our second argument, it will be included in the resulting type.
Pro tip: The second argument to Extract
can also be a union!
Since string
and number
are not assignable to Function,
they will not be
included in the resulting type. This results in FunctionArgs
evaluating to the
following type:
type FunctionArgs = () => string | () => number
filterProducts
Use case: Now that we understand the basic concept, let’s look at a real world example.
Say we have an array of products, each of which contains a key type
which is a
string to determine what type of object it is. This would look something like
this:
type Product = | { type: "book"; author: string } | { type: "movie"; producer: string } | { type: "appliance"; manufacturer: string }
Let’s say we want to create a function that takes an array of Product
s and
returns only those which match a specified type. This would look something like
this:
function filterProducts(products: Product[], type: Product["type"]) { return products.filter((item) => item.type === type)}
While this function will do exactly what we want at runtime, the type returned
from calling filterProducts(products, 'book')
will be Product[]
even though
we know that the resulting array won’t contain any movie
s or appliance
s.
With the power of Extract
, we can improve this:
function filterProducts<T extends Product, U extends T["type"]>( products: T[], type: U,) { return products.filter( (item): item is Extract<T, Record<"type", U>> => item.type === type, )}
There is a lot going on here, so let’s break it down. First, we’ve updated the
function to accept two generic arguments: T
and U
. TypeScript will infer the
value of these
generic arguments
since we have typed the function parameters using the generic variables. Since
each generic argument has a corresponding generic constraint, the function will
properly type check the arguments provided to the function like it did in the
non-generic example we looked at before.
Now that we have our generic arguments, we can add a
type predicate
to our filter function. Our type predicate allows us to instruct TypeScript
about the specific type of the argument passed to the function when the function
returns true
. Array.prototype.filter
will use this to narrow the type of the
resulting array.
The type that our type predicate is narrowing to is
Extract<T, Record<"type", U>>
, which probably looks a bit confusing at first.
The type we are extracting from is T
, which will be our array of Products that
we passed to our function. Record<"type", U>
will be an object that looks
something like this: { type: 'book' }
. Just like our previous example with
FunctionArgs
, Extract
will return a type containing all members that
{ type: 'book' }
is assignable to. Our final resulting type when calling our
function will look like this:
type Result = { type: "book"; author: string }[]
Neat, right!
Bonus: Extract with template literal types
For a little bonus, let’s explore a complex but extremely powerful way of using Extract with template literal types to extract keys from an object with keys matching a specific pattern.
Let’s say we have a Person
type that contains some details about the user.
type Person = { name: string email: string homePhone: number mobilePhone: number workPhone: number}
Now, further suppose that we want to create a type from Person
that contains
only the phone number keys. This would be fairly simple with Pick
as we could
do this:
type PhoneInfo = Pick<Person, "homePhone" | "mobilePhone" | "workPhone">
However, if we have a large number of phone number keys, this will be difficult
to maintain and prone to errors. With Extract
and template literal types we
can create the PhoneInfo
type very easily with the following code:
type PhoneInfo = { [key in Extract<keyof Person, `${string}Phone`>]: Person[key]}
The resulting type of PhoneInfo
is the following:
type PhoneInfo = { homePhone: number mobilePhone: number workPhone: number}
I don’t know about you, but the power of type constructs like this is one of the reasons I love TypeScript!