Skip to content

feat: add Type Modifiers > Prickly Predicates appetizer #280

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

Merged
merged 1 commit into from
Jun 12, 2024
Merged
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
@@ -30,6 +30,7 @@ bibendum
bienvenidos
blandit
brewerton
Cactaceae
chuckie
clownsole
commodo
@@ -44,6 +45,7 @@ cum
curabitur
cursus
Ðâåà
dadgum
dapibus
dbccbd
diam
@@ -71,6 +73,7 @@ et
etiam
eu
euismod
Euphorbiaceae
facilisi
facilisis
fames
@@ -106,6 +109,7 @@ krusty
labore
lacinia
lacus
Lamiaceae
laoreet
lawyerings
lectus
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": ["."]
}
41 changes: 41 additions & 0 deletions projects/type-modifiers/prickly-predicates/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Prickly Predicates

> A [Learning TypeScript > Type Modifiers](https://learning-typescript.com/type-modifiers) 🥗 appetizer project.
Howdy ho, farmer friend!

Here at 🌵 Cultured Cacti 🌵, we cultivate the _crème de la crème_ of cactus, cassava, and chia.
Any plant that reminds you of savannahs, South America, and the American Southwest are our claim to fame.
Yee-haw!

We reckon our industry could be much improved by some spring cleaning of our code.
We'd like to prepare a trio of TypeScript type predicates to wrangle our unruly data types.
Are you up for the challenge, partner?

## Setup

In one terminal, run the TypeScript compiler via the `tsc` script within whichever step you're working on.
For example, to start the TypeScript compiler on the first step in watch mode:

```shell
npm run tsc -- --project 01-pruning-pests --watch
```

In another terminal, run Jest via the `test` script on whichever step you're working on.
For example, to start tests for the first step in watch mode:

```shell
npm run test -- 1 --watch
```

## Steps

- [1. Pruning Pests](./01-pruning-pests)
- [2. Plant Particulars](./02-plant-particulars)
- [3. Picking Pears](./03-picking-pears)

## Notes

- Don't import code from one step into another.
- For each type predicate function, explicitly write the return type with `is`
- Don't rely on [TypeScript 5.5's inferred type predicates](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5-beta/#inferred-type-predicates)
4 changes: 4 additions & 0 deletions projects/type-modifiers/prickly-predicates/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"label": "🥗 Prickly Predicates",
"position": 2
}
12 changes: 12 additions & 0 deletions projects/type-modifiers/prickly-predicates/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = {
transform: {
"^.+\\.(t|j)sx?$": [
"@swc/jest",
{
jsc: {
target: "es2021",
},
},
],
},
};
8 changes: 8 additions & 0 deletions projects/type-modifiers/prickly-predicates/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "prickly-predicates",
"scripts": {
"test": "jest",
"test:solutions": "cross-env TEST_SOLUTIONS=1 jest",
"tsc": "tsc"
}
}
4 changes: 4 additions & 0 deletions projects/type-modifiers/prickly-predicates/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.json",
"include": ["."]
}
2 changes: 0 additions & 2 deletions projects/type-modifiers/type-force/README.md
Original file line number Diff line number Diff line change
@@ -57,5 +57,3 @@ The `duel` function's return type should be a read-only tuple containing two ele
## Notes

- The existing code has correct runtime behavior. You'll need to add type annotations to it, but don't delete any existing code.

<!-- todo add type predicate appetizer to modifiers-of-the-types -->
2 changes: 1 addition & 1 deletion projects/type-modifiers/type-force/_category_.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"label": "🍲 Type Force",
"position": 2
"position": 3
}