Back Thu 29 Oct, 2020

Type | Treat: Challenge 1

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.

  1. Extracting a type definition out of a collection
  2. 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

Link to my full 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

Link to my full solution

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

Link to my full solution.

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, 👋.