Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Type Modifiers > Prickly Predicates appetizer #280

Merged
merged 1 commit into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading