Skip to content

Commit a981101

Browse files
feat: add Type Modifiers > Prickly Predicates appetizer (#280)
1 parent f0967f4 commit a981101

23 files changed

+417
-3
lines changed

dictionary.txt

+4
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ bibendum
3030
bienvenidos
3131
blandit
3232
brewerton
33+
Cactaceae
3334
chuckie
3435
clownsole
3536
commodo
@@ -44,6 +45,7 @@ cum
4445
curabitur
4546
cursus
4647
Ðâåà
48+
dadgum
4749
dapibus
4850
dbccbd
4951
diam
@@ -71,6 +73,7 @@ et
7173
etiam
7274
eu
7375
euismod
76+
Euphorbiaceae
7477
facilisi
7578
facilisis
7679
fames
@@ -106,6 +109,7 @@ krusty
106109
labore
107110
lacinia
108111
lacus
112+
Lamiaceae
109113
laoreet
110114
lawyerings
111115
lectus
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Step 1: Pruning Pests
2+
3+
Thanks for signing on to the farm, friend!
4+
That's mighty kind of you.
5+
We sure do appreciate it.
6+
7+
What we'll need from you first is help narrowing down the names of our fruits.
8+
We know what we grow, but these darn type systems don't.
9+
Can you help us out with a function to return whether a string is a known crop name?
10+
11+
## Specification
12+
13+
Export a type predicate function named `isCropName` that takes in a name of type `string`.
14+
It should return whether the data is one of the keys of the type of the existing `cropFamilies` object.
15+
16+
## Files
17+
18+
- `index.ts`: Write your `isCropName` function here
19+
- `index.test.ts`: Tests verifying `isCropName`
20+
- `solution.ts`: Solution code
21+
22+
## Notes
23+
24+
- The function's return type should be an explicit type predicate with the `is` keyword
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, expect, it, test } from "@jest/globals";
2+
import { expectType } from "tsd";
3+
4+
import * as index from "./index";
5+
import * as solution from "./solution";
6+
7+
const { isCropName } = process.env.TEST_SOLUTIONS
8+
? solution
9+
: (index as typeof solution);
10+
11+
describe(isCropName, () => {
12+
describe("types", () => {
13+
test("function type", () => {
14+
expectType<(name: string) => name is keyof typeof solution.cropFamilies>(
15+
isCropName
16+
);
17+
});
18+
});
19+
20+
it.each([
21+
["", false],
22+
["dandelion", false],
23+
["purslane", false],
24+
["cactus", true],
25+
["cassava", true],
26+
["chia", true],
27+
])("when given %j, returns %j", (input, expected) => {
28+
expect(isCropName(input)).toBe(expected);
29+
});
30+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const cropFamilies = {
2+
cactus: "Cactaceae",
3+
cassava: "Euphorbiaceae",
4+
chia: "Lamiaceae",
5+
};
6+
7+
// Write your isCropName function here! ✨
8+
// You'll need to export it so the tests can run it.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const cropFamilies = {
2+
cactus: "Cactaceae",
3+
cassava: "Euphorbiaceae",
4+
chia: "Lamiaceae",
5+
};
6+
7+
export function isCropName(name: string): name is keyof typeof cropFamilies {
8+
return name in cropFamilies;
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../../../../tsconfig.json",
3+
"include": ["."]
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Step 2: Plant Particulars
2+
3+
Well, I'll be darned!
4+
You blew through that first step faster than a dog chasing a roadrunner.
5+
Hee-yah!
6+
7+
Our second request of you is to deal with is weeding.
8+
We're sick and tired of these invasive weeds in our dadgum farm!
9+
They're just about as welcome as a rattlesnake at a square dance.
10+
11+
Can you help us write a function that filters data to just a known crop we want to grow?
12+
That'd be mighty useful in helping us skedaddle out those worrisome weeds.
13+
14+
## Specification
15+
16+
Export a type predicate function named `isAnyCrop` that takes in data of type `unknown`.
17+
It should return whether the data is an object that matches the existing `AnyCrop` interface.
18+
19+
> Tip: when a value is type `object`, TypeScript won't allow you to access a property unless you check first that the property's key is `in` the value:
20+
>
21+
> ```ts
22+
> function checkValue(value: unknown) {
23+
> if (!!value && typeof value === "object" && "key" in value) {
24+
> console.log(value.key);
25+
> }
26+
> }
27+
> ```
28+
29+
## Files
30+
31+
- `index.ts`: Write your `isAnyCrop` function here
32+
- `index.test.ts`: Tests verifying `isAnyCrop`
33+
- `solution.ts`: Solution code
34+
35+
## Notes
36+
37+
- The function's return type should be an explicit type predicate with the `is` keyword
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { describe, expect, it, test } from "@jest/globals";
2+
import { expectType } from "tsd";
3+
4+
import * as index from "./index";
5+
import * as solution from "./solution";
6+
7+
const { isAnyCrop } = process.env.TEST_SOLUTIONS
8+
? solution
9+
: (index as typeof solution);
10+
11+
describe(isAnyCrop, () => {
12+
describe("types", () => {
13+
test("function type", () => {
14+
expectType<(data: solution.AnyCrop) => data is solution.AnyCrop>(
15+
isAnyCrop
16+
);
17+
});
18+
});
19+
20+
it.each([
21+
[null, false],
22+
[undefined, false],
23+
["", false],
24+
[123, false],
25+
[[], false],
26+
[{}, false],
27+
[{ growth: null }, false],
28+
[{ growth: 123 }, false],
29+
[{ harvested: true }, false],
30+
[{ name: "cactus" }, false],
31+
[{ growth: null, harvested: true, name: "cactus" }, false],
32+
[{ growth: 5, harvested: null, name: "cactus" }, false],
33+
[{ growth: 5, harvested: true, name: null }, false],
34+
[{ growth: 5, harvested: true, name: "other" }, false],
35+
[{ growth: 5, harvested: true, name: "cactus" }, true],
36+
[{ growth: 5, harvested: true, name: "cassava" }, true],
37+
[{ growth: 5, harvested: true, name: "chia" }, true],
38+
])("when given %j, returns %j", (input, expected) => {
39+
expect(isAnyCrop(input)).toBe(expected);
40+
});
41+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface AnyCrop {
2+
growth: number;
3+
harvested: boolean;
4+
name: "cactus" | "cassava" | "chia";
5+
}
6+
7+
// Write your isAnyCrop function here! ✨
8+
// You'll need to export it so the tests can run it.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export interface AnyCrop {
2+
growth: number;
3+
harvested: boolean;
4+
name: "cactus" | "cassava" | "chia";
5+
}
6+
7+
export function isAnyCrop(data: unknown): data is AnyCrop {
8+
return (
9+
!!data &&
10+
typeof data === "object" &&
11+
"growth" in data &&
12+
typeof data.growth === "number" &&
13+
"harvested" in data &&
14+
typeof data.harvested === "boolean" &&
15+
"name" in data &&
16+
typeof data.name === "string" &&
17+
["cactus", "cassava", "chia"].includes(data.name)
18+
);
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../../../../tsconfig.json",
3+
"include": ["."]
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Step 3: Picking Pears
2+
3+
Well, narrow my types and call me a structurally matched type.
4+
Aren't you just the most type-safe cowboy this side of the Sammamish River!
5+
6+
Our next and final request of you is to deal with a juicy one.
7+
You're going to help us harvest some succulent cactus pears!
8+
They make a mighty fine jam, if I do say so myself.
9+
10+
We can give you a whole array of potential cacti.
11+
We'll need you to return back all the cacti with fruits.
12+
13+
## Specification
14+
15+
Export two functions:
16+
17+
- `isFruitBearingCactus`: a type predicate function named that takes in data of the provided `Cactus` interface and returns whether data is type `FruitBearingCactus`
18+
- `pickFruitBearingCacti`: a function that takes an array of `Cactus` objects and returns an array consisting of all the `FruitBearingCactus` elements
19+
20+
## Files
21+
22+
- `index.ts`: Write your `isFruitBearingCactus` and `pickFruitBearingCacti` functions here
23+
- `index.test.ts`: Tests verifying `isFruitBearingCactus` and `pickFruitBearingCacti`
24+
- `solution.ts`: Solution code
25+
26+
## Notes
27+
28+
- The function's return type should be an explicit type predicate with the `is` keyword
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { describe, expect, it, test } from "@jest/globals";
2+
import { expectType } from "tsd";
3+
4+
import * as index from "./index";
5+
import * as solution from "./solution";
6+
7+
const { isFruitBearingCactus, pickFruitBearingCacti } = process.env
8+
.TEST_SOLUTIONS
9+
? solution
10+
: (index as typeof solution);
11+
12+
describe(isFruitBearingCactus, () => {
13+
describe("types", () => {
14+
test("function type", () => {
15+
expectType<
16+
(data: solution.Cactus) => data is solution.FruitBearingCactus
17+
>(isFruitBearingCactus);
18+
});
19+
});
20+
21+
it.each<[solution.Cactus, boolean]>([
22+
[{ picked: false, state: "dormant" }, false],
23+
[{ picked: true, state: "dormant" }, false],
24+
[{ flowers: "small", state: "flowering" }, false],
25+
[{ flowers: "medium", state: "flowering" }, false],
26+
[{ flowers: "large", state: "flowering" }, false],
27+
[{ fruits: 0, state: "fruit-bearing" }, true],
28+
[{ fruits: 1, state: "fruit-bearing" }, true],
29+
[{ fruits: 2, state: "fruit-bearing" }, true],
30+
])("when given %j, returns %j", (input, expected) => {
31+
expect(isFruitBearingCactus(input)).toBe(expected);
32+
});
33+
});
34+
35+
describe(pickFruitBearingCacti, () => {
36+
describe("types", () => {
37+
test("function type", () => {
38+
expectType<(data: solution.Cactus[]) => solution.FruitBearingCactus[]>(
39+
pickFruitBearingCacti
40+
);
41+
});
42+
});
43+
44+
it.each<[solution.Cactus[], solution.Cactus[]]>([
45+
[[], []],
46+
[[{ picked: true, state: "dormant" }], []],
47+
[[{ flowers: "small", state: "flowering" }], []],
48+
[[{ flowers: "medium", state: "flowering" }], []],
49+
[[{ flowers: "large", state: "flowering" }], []],
50+
[
51+
[{ fruits: 0, state: "fruit-bearing" }],
52+
[{ fruits: 0, state: "fruit-bearing" }],
53+
],
54+
[
55+
[{ fruits: 1, state: "fruit-bearing" }],
56+
[{ fruits: 1, state: "fruit-bearing" }],
57+
],
58+
[
59+
[{ fruits: 2, state: "fruit-bearing" }],
60+
[{ fruits: 2, state: "fruit-bearing" }],
61+
],
62+
[
63+
[
64+
{ picked: true, state: "dormant" },
65+
{ flowers: "small", state: "flowering" },
66+
],
67+
[],
68+
],
69+
[
70+
[
71+
{ picked: true, state: "dormant" },
72+
{ flowers: "small", state: "flowering" },
73+
{ flowers: "medium", state: "flowering" },
74+
{ flowers: "large", state: "flowering" },
75+
{ fruits: 0, state: "fruit-bearing" },
76+
],
77+
[{ fruits: 0, state: "fruit-bearing" }],
78+
],
79+
])("when given %j, returns %j", (input, expected) => {
80+
expect(pickFruitBearingCacti(input)).toEqual(expected);
81+
});
82+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export type Cactus = DefaultCactus | FloweringCactus | FruitBearingCactus;
2+
3+
export interface FloweringCactus {
4+
flowers: "small" | "medium" | "large";
5+
state: "flowering";
6+
}
7+
8+
export interface FruitBearingCactus {
9+
fruits: number;
10+
state: "fruit-bearing";
11+
}
12+
13+
export interface DefaultCactus {
14+
picked: boolean;
15+
state: "default";
16+
}
17+
18+
// Write your isFruitBearingCactus and pickFruitBearingCacti functions here! ✨
19+
// You'll need to export it so the tests can run it.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export type Cactus = DormantCactus | FloweringCactus | FruitBearingCactus;
2+
3+
export interface DormantCactus {
4+
picked: boolean;
5+
state: "dormant";
6+
}
7+
8+
export interface FloweringCactus {
9+
flowers: "small" | "medium" | "large";
10+
state: "flowering";
11+
}
12+
13+
export interface FruitBearingCactus {
14+
fruits: number;
15+
state: "fruit-bearing";
16+
}
17+
18+
export function isFruitBearingCactus(
19+
cactus: Cactus
20+
): cactus is FruitBearingCactus {
21+
return cactus.state === "fruit-bearing";
22+
}
23+
24+
export function pickFruitBearingCacti(cacti: Cactus[]) {
25+
return cacti.filter(isFruitBearingCactus);
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../../../../tsconfig.json",
3+
"include": ["."]
4+
}

0 commit comments

Comments
 (0)