I've been looking for something to write/post about recently. Earlier in the week I recall seeing that TypeScript are running a set of daily challenges, with a "spooky" Halloween theme. We adopted TypeScript at Unmade a couple of years ago, so I've used it enough to feel capable with its various features. But as with anything, there's always room to learn and improve your understanding, regardless of how long you've used a thing. So with that in mind, I thought I'd use these challenges as a means of brushing up my know-how, and getting into the habit of writing more.
I'll aim to undertake the challenges as they come out, or as soon as possible thereafter. I'll share my solutions on the #TypeOrTreat hashtag, but I'll also include a link to a post on this site. This will allow me to dig into my solutions in a bit more detail. Let's see how this goes.
Click here to see full details of the challenge.
The Challenge
There are 2 parts to the challenge.
- Extracting a type definition out of a collection
- Deriving specialised types from other types
1. Extracting a type definition out of a collection
Given the following type definition of an API response
type GhostAPIResponse = {
name: string;
birthDate?: string;
deathDate?: string;
bio: string;
hauntings: Array<{ title: string; provenance: string; location: string }>;
};
and the function
const displayHauntings = (haunting: any) => {
console.log(` - Title: ${haunting.title}`);
console.log(` ${haunting.location}`);
console.log(` ${haunting.provenance}`);
};
Create a type definition for the haunting
argument, without modifying GhostAPIResponse
.
My Solution
Cutting right to the chase
// defining this
type Haunting = GhostAPIResponse['hauntings'][0];
// allows you to change
const displayHauntings = (haunting: any) => {...};
// to
const displayHauntings = (haunting: Haunting) => {...};
Let's break that down a bit
GhostAPIResponse["hauntings"];
This looks like regular property access, same as you would use in plain JavaScript to get whatever value is at that location. But this being TypeScript, we don't have values, we have types. This means that we get back the type of whatever is at that location. By looking at GhostAPIResponse
we can see that the type is
Array<{ title: string; provenance: string; location: string }>
We're getting closer to what we need! We now have an array, but what we want is the type of the items inside the array. We can obtain that in much the same way as got us this far. As well as using strings for indexed access, we can also use numbers. Looking at the 0th item from the array will return the type of that item, which is exactly what we're after. We can use that to declare a Haunting
type, which we can use to replace the any
on the displayHaunting
function argument.
Bonus
When using indexed access, the supplied index is itself treated as a type. This doesn't really change anything when using the string "hauntings"
, as there is no other type that can be supplied in its place. But when we're accessing array items, the index we supply will always be of a particular type...number
! This means we could also define the Haunting
type as follows
type Haunting = GhostAPIResponse["hauntings"][number];
2. Deriving specialised types from other types
Given a set of types that describe various treats or tricks, and a union of all of those types, derive sub-types which contain only specific matching types. Specifically, given
// candy
type SnackBars = { name: "Short Chocolate Bars"; amount: 4; candy: true };
type Apples = { name: "Apples"; candy: true };
type Cookies = { name: "Cookies"; candy: true; peanuts: true };
type SnickersBar = { name: "Snickers Bar"; candy: true; peanuts: true };
type Gumballs = {
name: "Gooey Gumballs";
color: "green" | "purples";
candy: true;
};
// tricks
type Toothpaste = { name: "Toothpaste"; minty: true; trick: true };
type Pencil = { name: "Pencil"; trick: true };
// our swag
type ResultsFromHalloween =
| SnackBars
| Gumballs
| Apples
| SnickersBar
| Cookies
| Toothpaste
| Pencil;
Define the following
type AllCandies = ...
type AllTricks = ...
type AllCandiesWithoutPeanuts = ...
My Solution
There were 2 ways of doing this that I landed upon, I'll outline both. Firstly, by following the guidance in the challenge, which suggests using Conditional Types, and a variation of that which uses the built-in utility types, Extract
and Exclude
.
Conditional Types
type Candy = { candy: true };
type IsCandy<T> = T extends Candy ? T : never;
type Trick = { trick: true };
type IsTrick<T> = T extends Trick ? T : never;
type Peanut = { peanuts: true };
type WithoutPeanuts<T> = T extends Peanut ? never : T;
// the types we need to define
type AllCandies = IsCandy<ResultsFromHalloween>;
type AllTricks = IsTrick<ResultsFromHalloween>;
type AllCandiesWithoutPeanuts = WithoutPeanuts<AllCandies>;
The approach for all 3 types is pretty much the same, so I'll just cover AllCandies
.
First we create the minimal type definition that describes candy
type Candy = { candy: true };
Using this, we can create a generic helper that will allow us to pluck only the things that match Candy
from whatever we supply
type IsCandy<T> = T extends Candy ? T : never;
The type T
will be supplied when IsCandy
is used. If that supplied type extends Candy
, it will be returned, otherwise it is disregarded. A type that extends Candy
is another way of saying, "the thing you have supplied has a candy
property, and its value is true
".
With this, we can check all of the swag from trick or treating and split out only the candy
type AllCandies = IsCandy<ResultsFromHalloween>;
The result of this is that the type of AllCandies
is
SnackBars | Gumballs | Apples | SnickersBar | Cookies;
which you can see, is a union of only those types that have candy: true
.
Extract and Exclude
TypeScript has a bunch of useful types built in which can oftentimes result in more succinct code. Using Extract
and Exclude
to solve this challenge, we end up with
type Candy = { candy: true };
type Trick = { trick: true };
type Peanut = { peanuts: true };
// the types we need to define
type AllCandies = Extract<ResultsFromHalloween, Candy>;
type AllTricks = Extract<ResultsFromHalloween, Trick>;
type AllCandiesWithoutPeanuts = Exclude<AllCandies, Peanut>;
As the names suggest, Extract
and Exclude
are effectively the inverse of each other. They work by either selecting, or disgarding, the types that match the second argument. In other words,
Extract<ResultsFromHalloween, Candy>;
will select from ResultsFromHalloween
, only the items that are assignable to Candy
. Being assignable to is effectively the same as extending from (i.e. if A extends B
, then A
is said to be assignable to B
).
Wrapping up
If you made it this far, thanks for reading. If there's any feedback or questions about anything I've written here, give me a shout on Twitter @jazlalli1. Until next time, 👋.