diff --git a/dictionary.txt b/dictionary.txt index e4e4b2a6..5c616db9 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -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 diff --git a/projects/type-modifiers/prickly-predicates/01-pruning-pests/README.md b/projects/type-modifiers/prickly-predicates/01-pruning-pests/README.md new file mode 100644 index 00000000..78eeb67f --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/01-pruning-pests/README.md @@ -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 diff --git a/projects/type-modifiers/prickly-predicates/01-pruning-pests/index.test.ts b/projects/type-modifiers/prickly-predicates/01-pruning-pests/index.test.ts new file mode 100644 index 00000000..c61fd915 --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/01-pruning-pests/index.test.ts @@ -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); + }); +}); diff --git a/projects/type-modifiers/prickly-predicates/01-pruning-pests/index.ts b/projects/type-modifiers/prickly-predicates/01-pruning-pests/index.ts new file mode 100644 index 00000000..5db8c782 --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/01-pruning-pests/index.ts @@ -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. diff --git a/projects/type-modifiers/prickly-predicates/01-pruning-pests/solution.ts b/projects/type-modifiers/prickly-predicates/01-pruning-pests/solution.ts new file mode 100644 index 00000000..17ad41f2 --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/01-pruning-pests/solution.ts @@ -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; +} diff --git a/projects/type-modifiers/prickly-predicates/01-pruning-pests/tsconfig.json b/projects/type-modifiers/prickly-predicates/01-pruning-pests/tsconfig.json new file mode 100644 index 00000000..25716e06 --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/01-pruning-pests/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../../tsconfig.json", + "include": ["."] +} diff --git a/projects/type-modifiers/prickly-predicates/02-plant-particulars/README.md b/projects/type-modifiers/prickly-predicates/02-plant-particulars/README.md new file mode 100644 index 00000000..9c29949c --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/02-plant-particulars/README.md @@ -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 diff --git a/projects/type-modifiers/prickly-predicates/02-plant-particulars/index.test.ts b/projects/type-modifiers/prickly-predicates/02-plant-particulars/index.test.ts new file mode 100644 index 00000000..5702d9e7 --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/02-plant-particulars/index.test.ts @@ -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); + }); +}); diff --git a/projects/type-modifiers/prickly-predicates/02-plant-particulars/index.ts b/projects/type-modifiers/prickly-predicates/02-plant-particulars/index.ts new file mode 100644 index 00000000..ae237bbe --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/02-plant-particulars/index.ts @@ -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. diff --git a/projects/type-modifiers/prickly-predicates/02-plant-particulars/solution.ts b/projects/type-modifiers/prickly-predicates/02-plant-particulars/solution.ts new file mode 100644 index 00000000..5206e01d --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/02-plant-particulars/solution.ts @@ -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) + ); +} diff --git a/projects/type-modifiers/prickly-predicates/02-plant-particulars/tsconfig.json b/projects/type-modifiers/prickly-predicates/02-plant-particulars/tsconfig.json new file mode 100644 index 00000000..25716e06 --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/02-plant-particulars/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../../tsconfig.json", + "include": ["."] +} diff --git a/projects/type-modifiers/prickly-predicates/03-picking-pears/README.md b/projects/type-modifiers/prickly-predicates/03-picking-pears/README.md new file mode 100644 index 00000000..2f061add --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/03-picking-pears/README.md @@ -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 diff --git a/projects/type-modifiers/prickly-predicates/03-picking-pears/index.test.ts b/projects/type-modifiers/prickly-predicates/03-picking-pears/index.test.ts new file mode 100644 index 00000000..6e2da441 --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/03-picking-pears/index.test.ts @@ -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); + }); +}); diff --git a/projects/type-modifiers/prickly-predicates/03-picking-pears/index.ts b/projects/type-modifiers/prickly-predicates/03-picking-pears/index.ts new file mode 100644 index 00000000..b33cca83 --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/03-picking-pears/index.ts @@ -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. diff --git a/projects/type-modifiers/prickly-predicates/03-picking-pears/solution.ts b/projects/type-modifiers/prickly-predicates/03-picking-pears/solution.ts new file mode 100644 index 00000000..7819b05c --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/03-picking-pears/solution.ts @@ -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); +} diff --git a/projects/type-modifiers/prickly-predicates/03-picking-pears/tsconfig.json b/projects/type-modifiers/prickly-predicates/03-picking-pears/tsconfig.json new file mode 100644 index 00000000..25716e06 --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/03-picking-pears/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../../tsconfig.json", + "include": ["."] +} diff --git a/projects/type-modifiers/prickly-predicates/README.md b/projects/type-modifiers/prickly-predicates/README.md new file mode 100644 index 00000000..c763cddd --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/README.md @@ -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) diff --git a/projects/type-modifiers/prickly-predicates/_category_.json b/projects/type-modifiers/prickly-predicates/_category_.json new file mode 100644 index 00000000..6e3de425 --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "🥗 Prickly Predicates", + "position": 2 +} diff --git a/projects/type-modifiers/prickly-predicates/jest.config.js b/projects/type-modifiers/prickly-predicates/jest.config.js new file mode 100644 index 00000000..0bcb2272 --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/jest.config.js @@ -0,0 +1,12 @@ +module.exports = { + transform: { + "^.+\\.(t|j)sx?$": [ + "@swc/jest", + { + jsc: { + target: "es2021", + }, + }, + ], + }, +}; diff --git a/projects/type-modifiers/prickly-predicates/package.json b/projects/type-modifiers/prickly-predicates/package.json new file mode 100644 index 00000000..94299d00 --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/package.json @@ -0,0 +1,8 @@ +{ + "name": "prickly-predicates", + "scripts": { + "test": "jest", + "test:solutions": "cross-env TEST_SOLUTIONS=1 jest", + "tsc": "tsc" + } +} diff --git a/projects/type-modifiers/prickly-predicates/tsconfig.json b/projects/type-modifiers/prickly-predicates/tsconfig.json new file mode 100644 index 00000000..3fcafa94 --- /dev/null +++ b/projects/type-modifiers/prickly-predicates/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["."] +} diff --git a/projects/type-modifiers/type-force/README.md b/projects/type-modifiers/type-force/README.md index 9d7a17e8..18884f3a 100644 --- a/projects/type-modifiers/type-force/README.md +++ b/projects/type-modifiers/type-force/README.md @@ -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. - - diff --git a/projects/type-modifiers/type-force/_category_.json b/projects/type-modifiers/type-force/_category_.json index f77e8893..d43961c4 100644 --- a/projects/type-modifiers/type-force/_category_.json +++ b/projects/type-modifiers/type-force/_category_.json @@ -1,4 +1,4 @@ { "label": "🍲 Type Force", - "position": 2 + "position": 3 }