Skip to content

Commit

Permalink
feat: add Type Modifiers > Prickly Predicates appetizer (#280)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaKGoldberg authored Jun 12, 2024
1 parent f0967f4 commit a981101
Show file tree
Hide file tree
Showing 23 changed files with 417 additions and 3 deletions.
4 changes: 4 additions & 0 deletions dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ bibendum
bienvenidos
blandit
brewerton
Cactaceae
chuckie
clownsole
commodo
Expand All @@ -44,6 +45,7 @@ cum
curabitur
cursus
Ðâåà
dadgum
dapibus
dbccbd
diam
Expand Down Expand Up @@ -71,6 +73,7 @@ et
etiam
eu
euismod
Euphorbiaceae
facilisi
facilisis
fames
Expand Down Expand Up @@ -106,6 +109,7 @@ krusty
labore
lacinia
lacus
Lamiaceae
laoreet
lawyerings
lectus
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Step 1: Pruning Pests

Thanks for signing on to the farm, friend!
That's mighty kind of you.
We sure do appreciate it.

What we'll need from you first is help narrowing down the names of our fruits.
We know what we grow, but these darn type systems don't.
Can you help us out with a function to return whether a string is a known crop name?

## Specification

Export a type predicate function named `isCropName` that takes in a name of type `string`.
It should return whether the data is one of the keys of the type of the existing `cropFamilies` object.

## Files

- `index.ts`: Write your `isCropName` function here
- `index.test.ts`: Tests verifying `isCropName`
- `solution.ts`: Solution code

## Notes

- The function's return type should be an explicit type predicate with the `is` keyword
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it, test } from "@jest/globals";
import { expectType } from "tsd";

import * as index from "./index";
import * as solution from "./solution";

const { isCropName } = process.env.TEST_SOLUTIONS
? solution
: (index as typeof solution);

describe(isCropName, () => {
describe("types", () => {
test("function type", () => {
expectType<(name: string) => name is keyof typeof solution.cropFamilies>(
isCropName
);
});
});

it.each([
["", false],
["dandelion", false],
["purslane", false],
["cactus", true],
["cassava", true],
["chia", true],
])("when given %j, returns %j", (input, expected) => {
expect(isCropName(input)).toBe(expected);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const cropFamilies = {
cactus: "Cactaceae",
cassava: "Euphorbiaceae",
chia: "Lamiaceae",
};

// Write your isCropName function here! ✨
// You'll need to export it so the tests can run it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const cropFamilies = {
cactus: "Cactaceae",
cassava: "Euphorbiaceae",
chia: "Lamiaceae",
};

export function isCropName(name: string): name is keyof typeof cropFamilies {
return name in cropFamilies;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../../../tsconfig.json",
"include": ["."]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Step 2: Plant Particulars

Well, I'll be darned!
You blew through that first step faster than a dog chasing a roadrunner.
Hee-yah!

Our second request of you is to deal with is weeding.
We're sick and tired of these invasive weeds in our dadgum farm!
They're just about as welcome as a rattlesnake at a square dance.

Can you help us write a function that filters data to just a known crop we want to grow?
That'd be mighty useful in helping us skedaddle out those worrisome weeds.

## Specification

Export a type predicate function named `isAnyCrop` that takes in data of type `unknown`.
It should return whether the data is an object that matches the existing `AnyCrop` interface.

> 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:
>
> ```ts
> function checkValue(value: unknown) {
> if (!!value && typeof value === "object" && "key" in value) {
> console.log(value.key);
> }
> }
> ```
## Files
- `index.ts`: Write your `isAnyCrop` function here
- `index.test.ts`: Tests verifying `isAnyCrop`
- `solution.ts`: Solution code
## Notes
- The function's return type should be an explicit type predicate with the `is` keyword
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, it, test } from "@jest/globals";
import { expectType } from "tsd";

import * as index from "./index";
import * as solution from "./solution";

const { isAnyCrop } = process.env.TEST_SOLUTIONS
? solution
: (index as typeof solution);

describe(isAnyCrop, () => {
describe("types", () => {
test("function type", () => {
expectType<(data: solution.AnyCrop) => data is solution.AnyCrop>(
isAnyCrop
);
});
});

it.each([
[null, false],
[undefined, false],
["", false],
[123, false],
[[], false],
[{}, false],
[{ growth: null }, false],
[{ growth: 123 }, false],
[{ harvested: true }, false],
[{ name: "cactus" }, false],
[{ growth: null, harvested: true, name: "cactus" }, false],
[{ growth: 5, harvested: null, name: "cactus" }, false],
[{ growth: 5, harvested: true, name: null }, false],
[{ growth: 5, harvested: true, name: "other" }, false],
[{ growth: 5, harvested: true, name: "cactus" }, true],
[{ growth: 5, harvested: true, name: "cassava" }, true],
[{ growth: 5, harvested: true, name: "chia" }, true],
])("when given %j, returns %j", (input, expected) => {
expect(isAnyCrop(input)).toBe(expected);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface AnyCrop {
growth: number;
harvested: boolean;
name: "cactus" | "cassava" | "chia";
}

// Write your isAnyCrop function here! ✨
// You'll need to export it so the tests can run it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface AnyCrop {
growth: number;
harvested: boolean;
name: "cactus" | "cassava" | "chia";
}

export function isAnyCrop(data: unknown): data is AnyCrop {
return (
!!data &&
typeof data === "object" &&
"growth" in data &&
typeof data.growth === "number" &&
"harvested" in data &&
typeof data.harvested === "boolean" &&
"name" in data &&
typeof data.name === "string" &&
["cactus", "cassava", "chia"].includes(data.name)
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../../../tsconfig.json",
"include": ["."]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Step 3: Picking Pears

Well, narrow my types and call me a structurally matched type.
Aren't you just the most type-safe cowboy this side of the Sammamish River!

Our next and final request of you is to deal with a juicy one.
You're going to help us harvest some succulent cactus pears!
They make a mighty fine jam, if I do say so myself.

We can give you a whole array of potential cacti.
We'll need you to return back all the cacti with fruits.

## Specification

Export two functions:

- `isFruitBearingCactus`: a type predicate function named that takes in data of the provided `Cactus` interface and returns whether data is type `FruitBearingCactus`
- `pickFruitBearingCacti`: a function that takes an array of `Cactus` objects and returns an array consisting of all the `FruitBearingCactus` elements

## Files

- `index.ts`: Write your `isFruitBearingCactus` and `pickFruitBearingCacti` functions here
- `index.test.ts`: Tests verifying `isFruitBearingCactus` and `pickFruitBearingCacti`
- `solution.ts`: Solution code

## Notes

- The function's return type should be an explicit type predicate with the `is` keyword
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { describe, expect, it, test } from "@jest/globals";
import { expectType } from "tsd";

import * as index from "./index";
import * as solution from "./solution";

const { isFruitBearingCactus, pickFruitBearingCacti } = process.env
.TEST_SOLUTIONS
? solution
: (index as typeof solution);

describe(isFruitBearingCactus, () => {
describe("types", () => {
test("function type", () => {
expectType<
(data: solution.Cactus) => data is solution.FruitBearingCactus
>(isFruitBearingCactus);
});
});

it.each<[solution.Cactus, boolean]>([
[{ picked: false, state: "dormant" }, false],
[{ picked: true, state: "dormant" }, false],
[{ flowers: "small", state: "flowering" }, false],
[{ flowers: "medium", state: "flowering" }, false],
[{ flowers: "large", state: "flowering" }, false],
[{ fruits: 0, state: "fruit-bearing" }, true],
[{ fruits: 1, state: "fruit-bearing" }, true],
[{ fruits: 2, state: "fruit-bearing" }, true],
])("when given %j, returns %j", (input, expected) => {
expect(isFruitBearingCactus(input)).toBe(expected);
});
});

describe(pickFruitBearingCacti, () => {
describe("types", () => {
test("function type", () => {
expectType<(data: solution.Cactus[]) => solution.FruitBearingCactus[]>(
pickFruitBearingCacti
);
});
});

it.each<[solution.Cactus[], solution.Cactus[]]>([
[[], []],
[[{ picked: true, state: "dormant" }], []],
[[{ flowers: "small", state: "flowering" }], []],
[[{ flowers: "medium", state: "flowering" }], []],
[[{ flowers: "large", state: "flowering" }], []],
[
[{ fruits: 0, state: "fruit-bearing" }],
[{ fruits: 0, state: "fruit-bearing" }],
],
[
[{ fruits: 1, state: "fruit-bearing" }],
[{ fruits: 1, state: "fruit-bearing" }],
],
[
[{ fruits: 2, state: "fruit-bearing" }],
[{ fruits: 2, state: "fruit-bearing" }],
],
[
[
{ picked: true, state: "dormant" },
{ flowers: "small", state: "flowering" },
],
[],
],
[
[
{ picked: true, state: "dormant" },
{ flowers: "small", state: "flowering" },
{ flowers: "medium", state: "flowering" },
{ flowers: "large", state: "flowering" },
{ fruits: 0, state: "fruit-bearing" },
],
[{ fruits: 0, state: "fruit-bearing" }],
],
])("when given %j, returns %j", (input, expected) => {
expect(pickFruitBearingCacti(input)).toEqual(expected);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export type Cactus = DefaultCactus | FloweringCactus | FruitBearingCactus;

export interface FloweringCactus {
flowers: "small" | "medium" | "large";
state: "flowering";
}

export interface FruitBearingCactus {
fruits: number;
state: "fruit-bearing";
}

export interface DefaultCactus {
picked: boolean;
state: "default";
}

// Write your isFruitBearingCactus and pickFruitBearingCacti functions here! ✨
// You'll need to export it so the tests can run it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export type Cactus = DormantCactus | FloweringCactus | FruitBearingCactus;

export interface DormantCactus {
picked: boolean;
state: "dormant";
}

export interface FloweringCactus {
flowers: "small" | "medium" | "large";
state: "flowering";
}

export interface FruitBearingCactus {
fruits: number;
state: "fruit-bearing";
}

export function isFruitBearingCactus(
cactus: Cactus
): cactus is FruitBearingCactus {
return cactus.state === "fruit-bearing";
}

export function pickFruitBearingCacti(cacti: Cactus[]) {
return cacti.filter(isFruitBearingCactus);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../../../tsconfig.json",
"include": ["."]
}
Loading

0 comments on commit a981101

Please sign in to comment.