Click here to see full details of the challenge.
The Challenge
As before, there are 2 parts to the challenge.
- Create type definitions using
typeof
- Narrowing down types
1. Create type definitions using typeof
Given the object and function below
const pumpkin = {
color: "yellowish",
type: "gourd",
size: "Medium",
energy: "109 Kj per 100g",
flowered: false,
sizeCM: "50.8 x 40.6 x 30.2 cm",
};
const createExamplePumpkin = () => {
return pumpkin;
};
use the typeof
operator to define the following types
type Pumpkin = ...
type PumpkinFromFunction = ...
My Solution
type Pumpkin = typeof pumpkin;
type PumpkinFromFunction = ReturnType<typeof createExamplePumpkin>;
You might be familiar with the typeof
from your JavaScript usage. However, rather than returning only one of the predefined values as per the JavaScript spec, TypeScript will give back the type definition that matches the operand. In the case of typeof pumpkin
, we get back the type definition that resembles the value of the pumpkin
variable
type Pumpkin = {
color: string;
type: string;
size: string;
energy: string;
flowered: boolean;
sizeCM: string;
};
For PumpkinFromFunction
, things are a little more involved. Firstly, typeof createExamplePumpkin
returns a function type
type PumpkinFunction = typeof createPumpkinFunction;
// gives us
type PumpkinFunction = () => {
color: string;
type: string;
size: string;
energy: string;
flowered: boolean;
sizeCM: string;
};
What we actually want is the type of the return value of that function, not of the function itself. We can't invoke the function in a type declaration. You could try and bypass this by doing the following
const pumpkinFromFunction = createExamplePumpkin();
type PumpkinFromFunction = typeof pumpkinFromFunction;
but invoking a function just to get its return value for a type declaration is a bit heavy-handed and not advisable. If the function has side-effects, then doing this would likely result in some weird behaviour in your application.
Given that we the type of the return value of the function, but don't want to invoke it, we need to look elsewhere. Handily, TypeScript offers a utility type for just that, namely, ReturnType
. We can supply the function type to it, in order to unwrap that functions return type
type PumpkinFromFunction = ReturnType<typeof createExamplePumpkin>;
2. Narrowing Down Types
Given ghosts of various types
type Vigo = { name: "Vigo Von Homburg Deutschendorf", born: "Moldova" humanIsh: true }
type Zuul = { name: "Zuul", demon: true sendBackToHell(): void }
type Vinz = { name: "Vinz Clortho", demon: true sendBackToHell(): void }
type Gizer = { name: "Vinz Clortho", god: true hijackStayPuffMan(): void }
type Slimer = { name: "Slimer", color: "Green-y see through", ectoplasmic: true }
type Ghosts = Vigo | Zuul | Vinz | Slimer
make the following function compile, without modifying it, by implementing areGods
, areDemons
, and areEctoPlasmic
.
function investigateReport(ghosts: Ghosts[]) {
if (areGods(ghosts)) {
// Unsure if is the right thing to do
// but it could work
crossTheStreams();
return;
}
// Tricky but something I think we can
// handle on a case-by-case basis
if (areDemons(ghosts)) {
for (const demon of ghosts) {
demon.sendBackToHell();
}
}
// We've done this a lot now,
// shouldn't be too difficult
if (areEctoPlasmic(ghosts)) {
shockAndTrap(ghosts);
}
}
My Solution
This one stumped me initally (hence the absence of a link to the solution, for now).
To start with, I followed the same approach as in the previous challenge, of creating narrower sub-types of ghosts using Extract
, as below
type God = Extract<Ghosts, { god: true }>;
type Demon = Extract<Ghosts, { demon: true }>;
type EctoPlasmic = Extract<Ghosts, { ectoplasmic: true }>;
From there, I knew I needed functions to fulfil the calls inside investigateReport
, with something like
function areGods(ghosts: Ghosts[]) {
return ghosts.every((ghost) => "god" in ghost);
}
function areDemons(ghosts: Ghosts[]) {
return ghosts.every((ghost) => "demon" in ghost);
}
function areEctoPlasmic(ghosts: Ghosts[]) {
return ghosts.every((ghost) => "ectoplasmic" in ghost);
}
These functions need to return true
or false
, based on whether the supplied ghosts
match the target type (the functions above do this). But that alone is not enough, as there are still compiler errors (as you can see here).
This is where I got stuck. After various amounts of poking around, I caved and took a peek at the solution, and then also the relevant bit of the docs. Turns out I was pretty close to a working solution. Yay for me!
Here is the full solution
function areGods(ghosts: Ghosts[]): ghosts is God[] {
return ghosts.every((ghost) => "god" in ghost);
}
function areDemons(ghosts: Ghosts[]): ghosts is Demon[] {
return ghosts.every((ghost) => "demon" in ghost);
}
function areEctoPlasmic(ghosts: Ghosts[]): ghosts is EctoPlasmic[] {
return ghosts.every((ghost) => "ectoplasmic" in ghost);
}
What I was missing was using the is
keyword. This is called a type predicate. The opening paragraph in this very helpful blogpost explains what they are for
Type predicates in TypeScript help you narrow down your types based on conditionals. Theyβre similar to type guards, but work on functions. They way the work is, if a function returns true, change the type of the paramter to something more useful.
So by specifying ghosts is Demon[]
, for example, we're telling TypeScript that whenever areDemons(ghosts)
evaluates to true, then ghosts
can be treated as a Demon[]
. This means that the code inside the if (areDemons(ghosts))
block is always operating on a Demon[]
, which satisfies the compiler, and removes all errors. Neat.
Wrapping Up
TIL about type predicates and the is
keyword. Useful for changing type definitions when some condition inside a function is met.
As usual, any questions or pointers, reach me on Twitter @jazlalli1. π