├── .github └── workflows │ ├── renovate-checks.yml │ └── section-repos.yml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── og-image-new.png ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── 01-branded-types │ ├── 00-intro.explainer.ts │ ├── 01-what-is-a-branded-type.explainer.ts │ ├── 02-form-validation.problem.ts │ ├── 02-form-validation.solution.ts │ ├── 03-entity-fetching.problem.ts │ ├── 03-entity-fetching.solution.ts │ ├── 04-reusable-valid-brand.problem.ts │ ├── 04-reusable-valid-brand.solution.ts │ ├── 05-currency-conversion.problem.ts │ ├── 05-currency-conversion.solution.ts │ ├── 06-index-signatures.problem.ts │ └── 06-index-signatures.solution.ts ├── 02-globals │ ├── 07-add-function-to-global-scope.problem.ts │ ├── 07-add-function-to-global-scope.solution.ts │ ├── 08-add-to-window.problem.ts │ ├── 08-add-to-window.solution.ts │ ├── 09-adding-to-process-env.problem.ts │ ├── 09-adding-to-process-env.solution.ts │ ├── 10-event-dispatcher.problem.1.ts │ ├── 10-event-dispatcher.problem.2.ts │ ├── 10-event-dispatcher.solution.1.ts │ └── 10-event-dispatcher.solution.2.ts ├── 03-type-predicates-assertion-functions │ ├── 12-type-predicates-with-filter.problem.ts │ ├── 12-type-predicates-with-filter.solution.1.ts │ ├── 12-type-predicates-with-filter.solution.2.ts │ ├── 13-assertion-functions.problem.ts │ ├── 13-assertion-functions.solution.ts │ ├── 14-typescripts-worst-error.problem.ts │ ├── 14-typescripts-worst-error.solution.ts │ ├── 15-type-predicates-with-generics.problem.ts │ ├── 15-type-predicates-with-generics.solution.ts │ ├── 16-brands-and-type-predicates.problem.ts │ ├── 16-brands-and-type-predicates.solution.ts │ ├── 17-brands-and-assertion-functions.problem.ts │ └── 17-brands-and-assertion-functions.solution.ts ├── 04-classes │ ├── 17.8-classes-as-types-and-value.problem.ts │ ├── 17.8-classes-as-types-and-value.solution.ts │ ├── 18-type-predicates-and-classes.problem.ts │ ├── 18-type-predicates-and-classes.solution.ts │ ├── 18.2-assertion-functions-and-classes.problem.ts │ ├── 18.2-assertion-functions-and-classes.solution.ts │ ├── 19.3-builder-pattern-intro.explainer.ts │ ├── 19.4-alex-trpc-builder-pattern.explainer.ts │ ├── 20-type-safe-map.problem.ts │ ├── 20-type-safe-map.solution.ts │ ├── 21-importance-of-default-generic.problem.ts │ ├── 21-importance-of-default-generic.solution.ts │ ├── 22-dynamic-middleware.problem.ts │ ├── 22-dynamic-middleware.solution.ts │ └── 22.5-subclassing-in-zod.explainer.ts ├── 05-external-libraries │ ├── 22.9-where-do-external-types-come-from.explainer.ts │ ├── 23-extract-external-lib-types.problem.ts │ ├── 23-extract-external-lib-types.solution.ts │ ├── 23.9-lodash-types.explainer.ts │ ├── 24-lodash-groupby.problem.ts │ ├── 24-lodash-groupby.solution.ts │ ├── 24.9-express-types.explainer.ts │ ├── 25-usage-with-express.problem.ts │ ├── 25-usage-with-express.solution.ts │ ├── 25.9-zod-types.explainer.ts │ ├── 26-usage-with-zod.problem.ts │ ├── 26-usage-with-zod.solution.1.ts │ ├── 26-usage-with-zod.solution.2.ts │ ├── 26.5-declaration.solution.d.ts │ ├── 26.5-override-external-lib-types.problem.ts │ └── 26.5-override-external-lib-types.solution.ts ├── 06-identity-functions │ ├── 27-const-annotations.problem.ts │ ├── 27-const-annotations.solution.1.ts │ ├── 27-const-annotations.solution.2.ts │ ├── 28-constraints-with-const-annotations.problem.ts │ ├── 28-constraints-with-const-annotations.solution.1.ts │ ├── 28-constraints-with-const-annotations.solution.2.ts │ ├── 29-finite-state-machine.problem.ts │ ├── 29-finite-state-machine.solution.ts │ ├── 30-no-generics-on-objects.problem.ts │ ├── 30-no-generics-on-objects.solution.1.ts │ ├── 30-no-generics-on-objects.solution.2.ts │ ├── 30.5-reverse-mapped-types.problem.ts │ └── 30.5-reverse-mapped-types.solution.ts ├── 07-challenges │ ├── 31-merge-dynamic-object-with-global.problem.ts │ ├── 31-merge-dynamic-object-with-global.solution.ts │ ├── 32-narrow-with-arrays.problem.ts │ ├── 32-narrow-with-arrays.solution.ts │ ├── 33-zod-with-express.problem.ts │ ├── 33-zod-with-express.solution.ts │ ├── 34-dynamic-reducer.problem.ts │ ├── 34-dynamic-reducer.solution.ts │ ├── 38-challenge-custom-jsx-element.problem.tsx │ └── 38-challenge-custom-jsx-element.solution.tsx ├── fake-animation-lib │ └── index.ts ├── fake-external-lib │ ├── fetches.ts │ ├── index.ts │ ├── nonNullable.ts │ └── typePredicates.ts └── helpers │ ├── Brand.ts │ └── type-utils.ts ├── tsconfig.json └── vite.config.mts /.github/workflows/renovate-checks.yml: -------------------------------------------------------------------------------- 1 | name: Renovate Checks 2 | on: 3 | push: 4 | branches: 5 | - "renovate/**" 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout Main 12 | uses: actions/checkout@v4 13 | with: 14 | ref: main 15 | path: repo 16 | 17 | - name: Install Dependencies in Main 18 | run: (cd repo && npm install) 19 | - name: Create Snapshot In Main 20 | run: (cd repo && npx tt-cli take-snapshot ./snap.md) 21 | - name: Copy Snapshot To Outer Directory 22 | run: mv repo/snap.md ./snap.md 23 | - name: Delete Main Directory 24 | run: rm -rf repo 25 | - name: Checkout Branch 26 | uses: actions/checkout@v4 27 | with: 28 | path: repo 29 | - name: Install Dependencies in Branch 30 | run: (cd repo && npm install) 31 | - name: Move Snapshot To Branch 32 | run: mv ./snap.md repo/snap.md 33 | - name: Compare Snapshot In Branch 34 | run: (cd repo && npx tt-cli compare-snapshot ./snap.md) 35 | -------------------------------------------------------------------------------- /.github/workflows/section-repos.yml: -------------------------------------------------------------------------------- 1 | name: Create Section Repos 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | run: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: 20.x 19 | - run: git config --global user.email "total-typescript@bot.com" 20 | - run: git config --global user.name "Total TypeScript Bot" 21 | - run: npx @total-typescript/exercise-cli@latest create-section-repos 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }} 24 | GH_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tsconfig.temp.json 3 | dist 4 | *.tsbuildinfo 5 | *.prompt.* 6 | .vscode/*.code-snippets -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "github.copilot.enable": { 5 | "*": false, 6 | }, 7 | "explorer.sortOrder": "mixed", 8 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Quickstart 4 | 5 | Clone this repo or [open in Gitpod](https://gitpod.io/#https://github.com/total-typescript/advanced-patterns-workshop). 6 | 7 | ```sh 8 | # Installs all dependencies 9 | npm install 10 | 11 | # Asks you which exercise you'd like to run, and runs it 12 | npm run exercise 13 | ``` 14 | 15 | ## How to take the course 16 | 17 | You'll notice that the course is split into exercises. Each exercise is split into a `*.problem` and a `*.solution`. 18 | 19 | To take an exercise: 20 | 21 | 1. Run `npm run exercise` 22 | 2. Choose which exercise you'd like to run. 23 | 24 | This course encourages **active, exploratory learning**. In the video, I'll explain a problem, and **you'll be asked to try to find a solution**. To attempt a solution, you'll need to: 25 | 26 | 1. Check out [TypeScript's docs](https://www.typescriptlang.org/docs/handbook/intro.html). 27 | 1. Try to find something that looks relevant. 28 | 1. Give it a go to see if it solves the problem. 29 | 30 | You'll know if you've succeeded because the tests will pass. 31 | 32 | **If you succeed**, or **if you get stuck**, unpause the video and check out the `*.solution`. You can see if your solution is better or worse than mine! 33 | 34 | ## Acknowledgements 35 | 36 | Say thanks to Matt on [Twitter](https://twitter.com/mattpocockuk) or by joining his [Discord](https://discord.gg/8S5ujhfTB3). Consider signing up to his [Total TypeScript course](https://totaltypescript.com). 37 | 38 | ## Reference 39 | 40 | ### `npm run exercise` 41 | 42 | Alias: `npm run e` 43 | 44 | Open a prompt for choosing which exercise you'd like to run. 45 | -------------------------------------------------------------------------------- /og-image-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/total-typescript/advanced-patterns-workshop/ef435383193c1eade50a637d1758fc738b0bee26/og-image-new.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advanced-patterns-workshop", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "Matt Pocock ", 6 | "license": "GPL-2.0", 7 | "devDependencies": { 8 | "@total-typescript/exercise-cli": "^0.11.0", 9 | "@types/node": "^18.6.5", 10 | "cross-fetch": "^3.1.5", 11 | "jsdom": "^25.0.0", 12 | "prettier": "^3.0.0", 13 | "typescript": "5.7.2", 14 | "vite-tsconfig-paths": "^5.0.0", 15 | "vitest": "^2.0.0" 16 | }, 17 | "scripts": { 18 | "exercise": "tt-cli run", 19 | "e": "npm run exercise", 20 | "solution": "tt-cli run --solution", 21 | "s": "npm run solution", 22 | "ci": "(cd scripts/tests && npx vitest run)", 23 | "prepare": "tt-cli prepare-stackblitz", 24 | "update-snapshots": "(cd scripts/tests && npx vitest run -u)", 25 | "e-00": "tt-cli run 00", 26 | "s-00": "tt-cli run 00 --solution", 27 | "e-01": "tt-cli run 01", 28 | "s-01": "tt-cli run 01 --solution", 29 | "e-02": "tt-cli run 02", 30 | "s-02": "tt-cli run 02 --solution", 31 | "e-03": "tt-cli run 03", 32 | "s-03": "tt-cli run 03 --solution", 33 | "e-04": "tt-cli run 04", 34 | "s-04": "tt-cli run 04 --solution", 35 | "e-05": "tt-cli run 05", 36 | "s-05": "tt-cli run 05 --solution", 37 | "e-06": "tt-cli run 06", 38 | "s-06": "tt-cli run 06 --solution", 39 | "e-07": "tt-cli run 07", 40 | "s-07": "tt-cli run 07 --solution", 41 | "e-08": "tt-cli run 08", 42 | "s-08": "tt-cli run 08 --solution", 43 | "e-09": "tt-cli run 09", 44 | "s-09": "tt-cli run 09 --solution", 45 | "e-10": "tt-cli run 10", 46 | "s-10": "tt-cli run 10 --solution", 47 | "e-12": "tt-cli run 12", 48 | "s-12": "tt-cli run 12 --solution", 49 | "e-13": "tt-cli run 13", 50 | "s-13": "tt-cli run 13 --solution", 51 | "e-14": "tt-cli run 14", 52 | "s-14": "tt-cli run 14 --solution", 53 | "e-15": "tt-cli run 15", 54 | "s-15": "tt-cli run 15 --solution", 55 | "e-16": "tt-cli run 16", 56 | "s-16": "tt-cli run 16 --solution", 57 | "e-17": "tt-cli run 17", 58 | "s-17": "tt-cli run 17 --solution", 59 | "e-17.8": "tt-cli run 17.8", 60 | "s-17.8": "tt-cli run 17.8 --solution", 61 | "e-18": "tt-cli run 18", 62 | "s-18": "tt-cli run 18 --solution", 63 | "e-18.2": "tt-cli run 18.2", 64 | "s-18.2": "tt-cli run 18.2 --solution", 65 | "e-19.3": "tt-cli run 19.3", 66 | "s-19.3": "tt-cli run 19.3 --solution", 67 | "e-19.4": "tt-cli run 19.4", 68 | "s-19.4": "tt-cli run 19.4 --solution", 69 | "e-20": "tt-cli run 20", 70 | "s-20": "tt-cli run 20 --solution", 71 | "e-21": "tt-cli run 21", 72 | "s-21": "tt-cli run 21 --solution", 73 | "e-22": "tt-cli run 22", 74 | "s-22": "tt-cli run 22 --solution", 75 | "e-22.5": "tt-cli run 22.5", 76 | "s-22.5": "tt-cli run 22.5 --solution", 77 | "e-22.9": "tt-cli run 22.9", 78 | "s-22.9": "tt-cli run 22.9 --solution", 79 | "e-23": "tt-cli run 23", 80 | "s-23": "tt-cli run 23 --solution", 81 | "e-23.9": "tt-cli run 23.9", 82 | "s-23.9": "tt-cli run 23.9 --solution", 83 | "e-24": "tt-cli run 24", 84 | "s-24": "tt-cli run 24 --solution", 85 | "e-24.9": "tt-cli run 24.9", 86 | "s-24.9": "tt-cli run 24.9 --solution", 87 | "e-25": "tt-cli run 25", 88 | "s-25": "tt-cli run 25 --solution", 89 | "e-25.9": "tt-cli run 25.9", 90 | "s-25.9": "tt-cli run 25.9 --solution", 91 | "e-26": "tt-cli run 26", 92 | "s-26": "tt-cli run 26 --solution", 93 | "e-26.5": "tt-cli run 26.5", 94 | "s-26.5": "tt-cli run 26.5 --solution", 95 | "e-27": "tt-cli run 27", 96 | "s-27": "tt-cli run 27 --solution", 97 | "e-28": "tt-cli run 28", 98 | "s-28": "tt-cli run 28 --solution", 99 | "e-29": "tt-cli run 29", 100 | "s-29": "tt-cli run 29 --solution", 101 | "e-30": "tt-cli run 30", 102 | "s-30": "tt-cli run 30 --solution", 103 | "e-30.5": "tt-cli run 30.5", 104 | "s-30.5": "tt-cli run 30.5 --solution", 105 | "e-31": "tt-cli run 31", 106 | "s-31": "tt-cli run 31 --solution", 107 | "e-32": "tt-cli run 32", 108 | "s-32": "tt-cli run 32 --solution", 109 | "e-33": "tt-cli run 33", 110 | "s-33": "tt-cli run 33 --solution", 111 | "e-34": "tt-cli run 34", 112 | "s-34": "tt-cli run 34 --solution", 113 | "e-38": "tt-cli run 38", 114 | "s-38": "tt-cli run 38 --solution" 115 | }, 116 | "dependencies": { 117 | "@types/express": "^4.17.14", 118 | "@types/lodash": "^4.14.186", 119 | "@types/react": "^18.0.21", 120 | "express": "^4.18.1", 121 | "lodash": "^4.17.21", 122 | "react": "^18.2.0", 123 | "ts-toolbelt": "^9.6.0", 124 | "zod": "^3.19.1" 125 | }, 126 | "type": "module" 127 | } 128 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "packageRules": [ 5 | { 6 | "packagePatterns": ["*"], 7 | "excludePackagePatterns": [ 8 | "typescript", 9 | "vitest", 10 | "jsdom", 11 | "prettier", 12 | "vite-tsconfig-paths", 13 | "react", 14 | "@types/react", 15 | "@total-typescript/exercise-cli", 16 | "zod" 17 | ], 18 | "enabled": false 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/01-branded-types/00-intro.explainer.ts: -------------------------------------------------------------------------------- 1 | // Matt explains the course 2 | 3 | export {}; 4 | -------------------------------------------------------------------------------- /src/01-branded-types/01-what-is-a-branded-type.explainer.ts: -------------------------------------------------------------------------------- 1 | import { Brand } from "../helpers/Brand"; 2 | 3 | type Password = Brand; 4 | type Email = Brand; 5 | 6 | type UserObject = Brand< 7 | { 8 | id: string; 9 | name: string; 10 | }, 11 | "User" 12 | >; 13 | 14 | const user: UserObject = { 15 | id: "awdawd", 16 | name: "awdawdawd", 17 | } as UserObject; 18 | 19 | const verifyPassword = (password: Password) => {}; 20 | 21 | const password = "1231423" as Password; 22 | 23 | const email = "mpocock@me.com" as Email; 24 | 25 | let passwordSlot: Password; 26 | 27 | passwordSlot = "awdjhawdjhbawd" as Password; 28 | -------------------------------------------------------------------------------- /src/01-branded-types/02-form-validation.problem.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { Brand } from "../helpers/Brand"; 3 | 4 | type Password = Brand; 5 | type Email = Brand; 6 | 7 | export const validateValues = (values: { email: string; password: string }) => { 8 | if (!values.email.includes("@")) { 9 | throw new Error("Email invalid"); 10 | } 11 | if (values.password.length < 8) { 12 | throw new Error("Password not long enough"); 13 | } 14 | 15 | return { 16 | email: values.email, 17 | password: values.password, 18 | }; 19 | }; 20 | 21 | const createUserOnApi = (values: { email: Email; password: Password }) => { 22 | // Imagine this function creates the user on the API 23 | }; 24 | 25 | const onSubmitHandler = (values: { email: string; password: string }) => { 26 | const validatedValues = validateValues(values); 27 | // How do we stop this erroring? 28 | createUserOnApi(validatedValues); 29 | }; 30 | 31 | describe("onSubmitHandler", () => { 32 | it("Should error if the email is invalid", () => { 33 | expect(() => { 34 | onSubmitHandler({ 35 | email: "invalid", 36 | password: "12345678", 37 | }); 38 | }).toThrowError("Email invalid"); 39 | }); 40 | 41 | it("Should error if the password is too short", () => { 42 | expect(() => { 43 | onSubmitHandler({ 44 | email: "whatever@example.com", 45 | password: "1234567", 46 | }); 47 | }).toThrowError("Password not long enough"); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/01-branded-types/02-form-validation.solution.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { Brand } from "../helpers/Brand"; 3 | 4 | type Password = Brand; 5 | type Email = Brand; 6 | 7 | export const validateValues = (values: { email: string; password: string }) => { 8 | if (!values.email.includes("@")) { 9 | throw new Error("Email invalid"); 10 | } 11 | if (values.password.length < 8) { 12 | throw new Error("Password not long enough"); 13 | } 14 | 15 | return { 16 | email: values.email as Email, 17 | password: values.password as Password, 18 | }; 19 | }; 20 | 21 | const createUserOnApi = (values: { email: Email; password: Password }) => { 22 | // Imagine this function creates the user on the API 23 | }; 24 | 25 | const onSubmitHandler = (values: { email: string; password: string }) => { 26 | const validatedValues = validateValues(values); 27 | createUserOnApi(validatedValues); 28 | }; 29 | 30 | describe("onSubmitHandler", () => { 31 | it("Should error if the email is invalid", () => { 32 | expect(() => { 33 | onSubmitHandler({ 34 | email: "invalid", 35 | password: "12345678", 36 | }); 37 | }).toThrowError("Email invalid"); 38 | }); 39 | 40 | it("Should error if the password is too short", () => { 41 | expect(() => { 42 | onSubmitHandler({ 43 | email: "whatever@example.com", 44 | password: "1234567", 45 | }); 46 | }).toThrowError("Password not long enough"); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/01-branded-types/03-entity-fetching.problem.ts: -------------------------------------------------------------------------------- 1 | import { it } from "vitest"; 2 | import { Brand } from "../helpers/Brand"; 3 | 4 | type UserId = Brand; 5 | type PostId = Brand; 6 | 7 | interface User { 8 | id: string; 9 | name: string; 10 | } 11 | 12 | interface Post { 13 | id: string; 14 | title: string; 15 | content: string; 16 | } 17 | 18 | const db: { users: User[]; posts: Post[] } = { 19 | users: [ 20 | { 21 | id: "1", 22 | name: "Miles", 23 | }, 24 | ], 25 | posts: [ 26 | { 27 | id: "1", 28 | title: "Hello world", 29 | content: "This is my first post", 30 | }, 31 | ], 32 | }; 33 | 34 | const getUserById = (id: string) => { 35 | return db.users.find((user) => user.id === id); 36 | }; 37 | 38 | const getPostById = (id: string) => { 39 | return db.posts.find((post) => post.id === id); 40 | }; 41 | 42 | it("Should only let you get a user by id with a user id", () => { 43 | const postId = "1" as PostId; 44 | 45 | // @ts-expect-error 46 | getUserById(postId); 47 | }); 48 | 49 | it("Should only let you get a post by id with a PostId", () => { 50 | const userId = "1" as UserId; 51 | 52 | // @ts-expect-error 53 | getPostById(userId); 54 | }); 55 | -------------------------------------------------------------------------------- /src/01-branded-types/03-entity-fetching.solution.ts: -------------------------------------------------------------------------------- 1 | import { it } from "vitest"; 2 | import { Brand } from "../helpers/Brand"; 3 | 4 | type UserId = Brand; 5 | type PostId = Brand; 6 | 7 | interface User { 8 | id: UserId; 9 | name: string; 10 | } 11 | 12 | interface Post { 13 | id: PostId; 14 | title: string; 15 | content: string; 16 | } 17 | 18 | const db: { users: User[]; posts: Post[] } = { 19 | users: [ 20 | { 21 | id: "1" as UserId, 22 | name: "Miles", 23 | }, 24 | ], 25 | posts: [ 26 | { 27 | id: "1" as PostId, 28 | title: "Hello world", 29 | content: "This is my first post", 30 | }, 31 | ], 32 | }; 33 | 34 | const getUserById = (id: UserId) => { 35 | return db.users.find((user) => user.id === id); 36 | }; 37 | 38 | const getPostById = (id: PostId) => { 39 | return db.posts.find((post) => post.id === id); 40 | }; 41 | 42 | it("Should only let you get a user by id with a user id", () => { 43 | const postId = "1" as PostId; 44 | 45 | // @ts-expect-error 46 | getUserById(postId); 47 | }); 48 | 49 | it("Should only let you get a post by id with a PostId", () => { 50 | const userId = "1" as UserId; 51 | 52 | // @ts-expect-error 53 | getPostById(userId); 54 | }); 55 | -------------------------------------------------------------------------------- /src/01-branded-types/04-reusable-valid-brand.problem.ts: -------------------------------------------------------------------------------- 1 | import { it } from "vitest"; 2 | import { Brand } from "../helpers/Brand"; 3 | 4 | type Valid = unknown; 5 | 6 | interface PasswordValues { 7 | password: string; 8 | confirmPassword: string; 9 | } 10 | 11 | const validatePassword = (values: PasswordValues) => { 12 | if (values.password !== values.confirmPassword) { 13 | throw new Error("Passwords do not match"); 14 | } 15 | 16 | return values; 17 | }; 18 | 19 | const createUserOnApi = (values: Valid) => { 20 | // Imagine this function creates the user on the API 21 | }; 22 | 23 | it("Should fail if you do not validate the values before calling createUserOnApi", () => { 24 | const onSubmitHandler = (values: PasswordValues) => { 25 | // @ts-expect-error 26 | createUserOnApi(values); 27 | }; 28 | }); 29 | 30 | it("Should succeed if you DO validate the values before calling createUserOnApi", () => { 31 | const onSubmitHandler = (values: PasswordValues) => { 32 | const validatedValues = validatePassword(values); 33 | createUserOnApi(validatedValues); 34 | }; 35 | }); 36 | -------------------------------------------------------------------------------- /src/01-branded-types/04-reusable-valid-brand.solution.ts: -------------------------------------------------------------------------------- 1 | import { it } from "vitest"; 2 | import { Brand } from "../helpers/Brand"; 3 | 4 | type Valid = Brand; 5 | 6 | interface PasswordValues { 7 | password: string; 8 | confirmPassword: string; 9 | } 10 | 11 | const validatePassword = (values: PasswordValues): Valid => { 12 | if (values.password !== values.confirmPassword) { 13 | throw new Error("Passwords do not match"); 14 | } 15 | 16 | return values as Valid; 17 | }; 18 | 19 | const createUserOnApi = (values: Valid) => { 20 | // Imagine this function creates the user on the API 21 | }; 22 | 23 | it("Should fail if you do not validate the values before calling createUserOnApi", () => { 24 | const onSubmitHandler = (values: PasswordValues) => { 25 | // @ts-expect-error 26 | createUserOnApi(values); 27 | }; 28 | }); 29 | 30 | it("Should succeed if you DO validate the values before calling createUserOnApi", () => { 31 | const onSubmitHandler = (values: PasswordValues) => { 32 | const validatedValues = validatePassword(values); 33 | createUserOnApi(validatedValues); 34 | }; 35 | }); 36 | -------------------------------------------------------------------------------- /src/01-branded-types/05-currency-conversion.problem.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "vitest"; 2 | import { Brand } from "../helpers/Brand"; 3 | 4 | interface User { 5 | id: string; 6 | name: string; 7 | maxConversionAmount: number; 8 | } 9 | 10 | // Mocks a function that uses an API to convert 11 | // One currency to another 12 | const getConversionRateFromApi = async ( 13 | amount: number, 14 | from: string, 15 | to: string, 16 | ) => { 17 | return Promise.resolve(amount * 0.82); 18 | }; 19 | 20 | // Mocks a function which actually performs the conversion 21 | const performConversion = async (user: User, to: string, amount: number) => {}; 22 | 23 | const ensureUserCanConvert = (user: User, amount: number): User => { 24 | if (user.maxConversionAmount < amount) { 25 | throw new Error("User cannot convert currency"); 26 | } 27 | 28 | return user; 29 | }; 30 | 31 | describe("Possible implementations", () => { 32 | it("Should error if you do not authorize the user first", () => { 33 | const handleConversionRequest = async ( 34 | user: User, 35 | from: string, 36 | to: string, 37 | amount: number, 38 | ) => { 39 | const convertedAmount = await getConversionRateFromApi(amount, from, to); 40 | 41 | // @ts-expect-error 42 | await performConversion(user, to, convertedAmount); 43 | }; 44 | }); 45 | 46 | it("Should error if you do not convert the amount first", () => { 47 | const handleConversionRequest = async ( 48 | user: User, 49 | from: string, 50 | to: string, 51 | amount: number, 52 | ) => { 53 | // @ts-expect-error 54 | const authorizedUser = ensureUserCanConvert(user, amount); 55 | 56 | // @ts-expect-error 57 | await performConversion(authorizedUser, to, amount); 58 | }; 59 | }); 60 | 61 | it("Should pass type checking if you authorize the user AND convert the amount", () => { 62 | const handleConversionRequest = async ( 63 | user: User, 64 | from: string, 65 | to: string, 66 | amount: number, 67 | ) => { 68 | const convertedAmount = await getConversionRateFromApi(amount, from, to); 69 | const authorizedUser = ensureUserCanConvert(user, convertedAmount); 70 | 71 | await performConversion(authorizedUser, to, convertedAmount); 72 | }; 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/01-branded-types/05-currency-conversion.solution.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "vitest"; 2 | import { Brand } from "../helpers/Brand"; 3 | 4 | interface User { 5 | id: string; 6 | name: string; 7 | maxConversionAmount: number; 8 | } 9 | 10 | type ConvertedAmount = Brand; 11 | type AuthorizedUser = Brand; 12 | 13 | // Mocks a function that uses an API to convert 14 | // One currency to another 15 | const getConversionRateFromApi = async ( 16 | amount: number, 17 | from: string, 18 | to: string, 19 | ) => { 20 | return Promise.resolve((amount * 0.82) as ConvertedAmount); 21 | }; 22 | 23 | // Mocks a function which actually performs the conversion 24 | const performConversion = async ( 25 | user: AuthorizedUser, 26 | to: string, 27 | amount: ConvertedAmount, 28 | ) => {}; 29 | 30 | const ensureUserCanConvert = ( 31 | user: User, 32 | amount: ConvertedAmount, 33 | ): AuthorizedUser => { 34 | if (user.maxConversionAmount < amount) { 35 | throw new Error("User cannot convert currency"); 36 | } 37 | 38 | return user as AuthorizedUser; 39 | }; 40 | 41 | describe("Possible implementations", () => { 42 | it("Should error if you do not authorize the user first", () => { 43 | const handleConversionRequest = async ( 44 | user: User, 45 | from: string, 46 | to: string, 47 | amount: number, 48 | ) => { 49 | const convertedAmount = await getConversionRateFromApi(amount, from, to); 50 | 51 | // @ts-expect-error 52 | await performConversion(user, to, convertedAmount); 53 | }; 54 | }); 55 | 56 | it("Should error if you do not convert the amount first", () => { 57 | const handleConversionRequest = async ( 58 | user: User, 59 | from: string, 60 | to: string, 61 | amount: number, 62 | ) => { 63 | // @ts-expect-error 64 | const authorizedUser = ensureUserCanConvert(user, amount); 65 | 66 | // @ts-expect-error 67 | await performConversion(authorizedUser, to, amount); 68 | }; 69 | }); 70 | 71 | it("Should pass type checking if you authorize the user AND convert the amount", () => { 72 | const handleConversionRequest = async ( 73 | user: User, 74 | from: string, 75 | to: string, 76 | amount: number, 77 | ) => { 78 | const convertedAmount = await getConversionRateFromApi(amount, from, to); 79 | const authorizedUser = ensureUserCanConvert(user, convertedAmount); 80 | 81 | await performConversion(authorizedUser, to, convertedAmount); 82 | }; 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/01-branded-types/06-index-signatures.problem.ts: -------------------------------------------------------------------------------- 1 | import { it } from "vitest"; 2 | import { Brand } from "../helpers/Brand"; 3 | import { Equal, Expect } from "../helpers/type-utils"; 4 | 5 | type PostId = Brand; 6 | type UserId = Brand; 7 | 8 | interface User { 9 | id: UserId; 10 | name: string; 11 | } 12 | 13 | interface Post { 14 | id: PostId; 15 | title: string; 16 | } 17 | 18 | /** 19 | * Change this type definition! We should be able to 20 | * add users and posts to the db by their id. 21 | * 22 | * You'll need an index signature of some kind - or maybe 23 | * two! 24 | */ 25 | const db: Record = {}; 26 | 27 | it("Should let you add users and posts to the db by their id", () => { 28 | const postId = "post_1" as PostId; 29 | const userId = "user_1" as UserId; 30 | 31 | db[postId] = { 32 | id: postId, 33 | title: "Hello world", 34 | }; 35 | 36 | db[userId] = { 37 | id: userId, 38 | name: "Miles", 39 | }; 40 | 41 | const test = () => { 42 | // Code slightly updated since video was recorded, see 43 | // https://gist.github.com/mattpocock/ac5bc4eabcb95c05d5d106ccb73c84cc 44 | const post = db[postId]; 45 | const user = db[userId]; 46 | 47 | type tests = [ 48 | Expect>, 49 | Expect>, 50 | ]; 51 | }; 52 | }); 53 | 54 | it("Should fail if you try to add a user under a post id", () => { 55 | const postId = "post_1" as PostId; 56 | const userId = "user_1" as UserId; 57 | 58 | const user: User = { 59 | id: userId, 60 | name: "Miles", 61 | }; 62 | 63 | // @ts-expect-error 64 | db[postId] = user; 65 | }); 66 | -------------------------------------------------------------------------------- /src/01-branded-types/06-index-signatures.solution.ts: -------------------------------------------------------------------------------- 1 | import { it } from "vitest"; 2 | import { Brand } from "../helpers/Brand"; 3 | import { Equal, Expect } from "../helpers/type-utils"; 4 | 5 | type PostId = Brand; 6 | type UserId = Brand; 7 | 8 | interface User { 9 | id: UserId; 10 | name: string; 11 | } 12 | 13 | interface Post { 14 | id: PostId; 15 | title: string; 16 | } 17 | 18 | const db: { 19 | [id: PostId]: Post; 20 | [id: UserId]: User; 21 | } = {}; 22 | 23 | it("Should let you add users and posts to the db by their id", () => { 24 | const postId = "post_1" as PostId; 25 | const userId = "user_1" as UserId; 26 | 27 | db[postId] = { 28 | id: postId, 29 | title: "Hello world", 30 | }; 31 | 32 | db[userId] = { 33 | id: userId, 34 | name: "Miles", 35 | }; 36 | 37 | const test = () => { 38 | const post = db[postId]; 39 | const user = db[userId]; 40 | 41 | type tests = [ 42 | Expect>, 43 | Expect>, 44 | ]; 45 | }; 46 | }); 47 | 48 | it("Should fail if you try to add a user under a post id", () => { 49 | const postId = "post_1" as PostId; 50 | const userId = "user_1" as UserId; 51 | 52 | const user: User = { 53 | id: userId, 54 | name: "Miles", 55 | }; 56 | 57 | // @ts-expect-error 58 | db[postId] = user; 59 | }); 60 | -------------------------------------------------------------------------------- /src/02-globals/07-add-function-to-global-scope.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | /** 5 | * Clues: 6 | * 7 | * 1. declare global will be needed: 8 | * 9 | * declare global {} 10 | * 11 | * 2. myFunc will need to be added to the global scope using 'function': 12 | * 13 | * function myFunc(): boolean 14 | * 15 | * 3. myVar will need to be added to the global scope using 'var': 16 | * 17 | * var myVar: number 18 | */ 19 | 20 | globalThis.myFunc = () => true; 21 | globalThis.myVar = 1; 22 | 23 | it("Should let you call myFunc without it being imported", () => { 24 | expect(myFunc()).toBe(true); 25 | type test1 = Expect boolean>>; 26 | }); 27 | 28 | it("Should let you access myVar without it being imported", () => { 29 | expect(myVar).toBe(1); 30 | type test1 = Expect>; 31 | }); 32 | -------------------------------------------------------------------------------- /src/02-globals/07-add-function-to-global-scope.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | /** 5 | * Because declare global works in multiple files, I've changed 6 | * the name of the variables/functions to avoid a conflict with 7 | * the other file 8 | */ 9 | declare global { 10 | function mySolutionFunc(): boolean; 11 | var mySolutionVar: number; 12 | } 13 | 14 | globalThis.mySolutionFunc = () => true; 15 | globalThis.mySolutionVar = 1; 16 | 17 | it("Should let you call myFunc without it being imported", () => { 18 | expect(mySolutionFunc()).toBe(true); 19 | type test1 = Expect boolean>>; 20 | }); 21 | 22 | it("Should let you access myVar without it being imported", () => { 23 | expect(mySolutionVar).toBe(1); 24 | type test1 = Expect>; 25 | }); 26 | -------------------------------------------------------------------------------- /src/02-globals/08-add-to-window.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | /** 5 | * Clues: 6 | * 7 | * 1. You'll need declare global again 8 | * 9 | * 2. Inside declare global, you'll need to modify the Window 10 | * interface to add a makeGreeting function 11 | */ 12 | 13 | window.makeGreeting = () => "Hello, world!"; 14 | 15 | it("Should let you call makeGreeting from the window object", () => { 16 | expect(window.makeGreeting()).toBe("Hello, world!"); 17 | 18 | type test1 = Expect string>>; 19 | }); 20 | -------------------------------------------------------------------------------- /src/02-globals/08-add-to-window.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | /** 5 | * Clues: 6 | * 7 | * 1. You'll need declare global again 8 | * 9 | * 2. Inside declare global, you'll need to modify the Window 10 | * interface to add a makeGreetingSolution function 11 | */ 12 | declare global { 13 | interface Window { 14 | makeGreetingSolution: () => string; 15 | } 16 | } 17 | 18 | window.makeGreetingSolution = () => "Hello, world!"; 19 | 20 | it("Should let you call makeGreetingSolution from the window object", () => { 21 | expect(window.makeGreetingSolution()).toBe("Hello, world!"); 22 | 23 | type test1 = Expect string>>; 24 | }); 25 | -------------------------------------------------------------------------------- /src/02-globals/09-adding-to-process-env.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | /** 5 | * Clues: 6 | * 7 | * 1. You'll need declare global again 8 | * 9 | * 2. You'll need to use the NodeJS namespace 10 | * 11 | * 3. Inside the NodeJS namespace, you'll need to add a 12 | * MY_ENV_VAR property to the ProcessEnv interface 13 | */ 14 | 15 | process.env.MY_ENV_VAR = "Hello, world!"; 16 | 17 | it("Should be declared as a string", () => { 18 | expect(process.env.MY_ENV_VAR).toEqual("Hello, world!"); 19 | }); 20 | 21 | it("Should NOT have undefined in the type", () => { 22 | const myVar = process.env.MY_ENV_VAR; 23 | type tests = [Expect>]; 24 | }); 25 | -------------------------------------------------------------------------------- /src/02-globals/09-adding-to-process-env.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | declare global { 5 | namespace NodeJS { 6 | interface ProcessEnv { 7 | MY_SOLUTION_ENV_VAR: string; 8 | } 9 | } 10 | } 11 | 12 | process.env.MY_SOLUTION_ENV_VAR = "Hello, world!"; 13 | 14 | it("Should be declared as a string", () => { 15 | expect(process.env.MY_SOLUTION_ENV_VAR).toEqual("Hello, world!"); 16 | }); 17 | 18 | it("Should NOT have undefined in the type", () => { 19 | const myVar = process.env.MY_SOLUTION_ENV_VAR; 20 | type tests = [Expect>]; 21 | }); 22 | -------------------------------------------------------------------------------- /src/02-globals/10-event-dispatcher.problem.1.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | /** 4 | * Here, we've actually got _multiple_ problem files! 5 | * Make sure to to check problem.2.ts too. 6 | */ 7 | 8 | declare global { 9 | interface DispatchableEvent { 10 | LOG_IN: { 11 | username: string; 12 | password: string; 13 | }; 14 | } 15 | 16 | /** 17 | * This type converts the DispatchableEvent 18 | * interface into a union: 19 | * 20 | * { type: 'LOG_IN'; username: string; password: string; } 21 | */ 22 | type UnionOfDispatchableEvents = { 23 | [K in keyof DispatchableEvent]: { 24 | type: K; 25 | } & DispatchableEvent[K]; 26 | }[keyof DispatchableEvent]; 27 | } 28 | 29 | const dispatchEvent = (event: UnionOfDispatchableEvents) => { 30 | // Imagine that this function dispatches this event 31 | // to a global handler 32 | }; 33 | 34 | it("Should be able to dispatch a LOG_IN and LOG_OUT event", () => { 35 | dispatchEvent({ type: "LOG_IN", username: "username", password: "password" }); 36 | dispatchEvent({ type: "LOG_OUT" }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/02-globals/10-event-dispatcher.problem.2.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | /** 4 | * How do we add a LOG_OUT and UPDATE_USERNAME events to the 5 | * DispatchableEvent interface from WITHIN 6 | * this file? 7 | */ 8 | 9 | const handler = (event: UnionOfDispatchableEvents) => { 10 | switch (event.type) { 11 | case "LOG_OUT": 12 | console.log("LOG_OUT"); 13 | break; 14 | case "UPDATE_USERNAME": 15 | console.log(event.username); 16 | break; 17 | } 18 | }; 19 | 20 | it("Should be able to handle LOG_OUT and UPDATE_USERNAME events", () => { 21 | handler({ type: "LOG_OUT" }); 22 | handler({ type: "UPDATE_USERNAME", username: "matt" }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/02-globals/10-event-dispatcher.solution.1.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | declare global { 4 | interface DispatchableEventSolution { 5 | LOG_IN: { 6 | username: string; 7 | password: string; 8 | }; 9 | } 10 | type UnionOfDispatchableEventsSolution = { 11 | [K in keyof DispatchableEventSolution]: { 12 | type: K; 13 | } & DispatchableEventSolution[K]; 14 | }[keyof DispatchableEventSolution]; 15 | } 16 | 17 | const dispatchEvent = (event: UnionOfDispatchableEventsSolution) => { 18 | // Imagine that this function dispatches this event 19 | // to a global handler 20 | }; 21 | 22 | it("Should be able to dispatch a LOG_IN and LOG_OUT event", () => { 23 | dispatchEvent({ type: "LOG_IN", username: "username", password: "password" }); 24 | dispatchEvent({ type: "LOG_OUT" }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/02-globals/10-event-dispatcher.solution.2.ts: -------------------------------------------------------------------------------- 1 | import { it } from "vitest"; 2 | 3 | declare global { 4 | interface DispatchableEventSolution { 5 | LOG_OUT: {}; 6 | UPDATE_USERNAME: { username: string }; 7 | } 8 | } 9 | 10 | const handler = (event: UnionOfDispatchableEventsSolution) => { 11 | switch (event.type) { 12 | case "LOG_OUT": 13 | console.log("LOG_OUT"); 14 | break; 15 | case "UPDATE_USERNAME": 16 | console.log(event.username); 17 | break; 18 | } 19 | }; 20 | 21 | it("Should be able to handle LOG_OUT and UPDATE_USERNAME events", () => { 22 | handler({ type: "LOG_OUT" }); 23 | handler({ type: "UPDATE_USERNAME", username: "matt" }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/03-type-predicates-assertion-functions/12-type-predicates-with-filter.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | export const values = ["a", "b", undefined, "c", undefined]; 5 | 6 | const filteredValues = values.filter((value) => Boolean(value)); 7 | 8 | it("Should filter out the undefined values", () => { 9 | expect(filteredValues).toEqual(["a", "b", "c"]); 10 | }); 11 | 12 | it('Should be of type "string[]"', () => { 13 | type test1 = Expect>; 14 | }); 15 | -------------------------------------------------------------------------------- /src/03-type-predicates-assertion-functions/12-type-predicates-with-filter.solution.1.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | export const values = ["a", "b", undefined, "c", undefined]; 5 | 6 | // This solution is a little ugly! The much better one 7 | // is solution number 2! 8 | const filteredValues = values.filter((value) => Boolean(value)) as string[]; 9 | 10 | it("Should filter out the undefined values", () => { 11 | expect(filteredValues).toEqual(["a", "b", "c"]); 12 | }); 13 | 14 | it('Should be of type "string[]"', () => { 15 | type test1 = Expect>; 16 | }); 17 | -------------------------------------------------------------------------------- /src/03-type-predicates-assertion-functions/12-type-predicates-with-filter.solution.2.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | export const values = ["a", "b", undefined, "c", undefined]; 5 | 6 | const filteredValues = values.filter((value): value is string => 7 | Boolean(value), 8 | ); 9 | 10 | it("Should filter out the undefined values", () => { 11 | expect(filteredValues).toEqual(["a", "b", "c"]); 12 | }); 13 | 14 | it('Should be of type "string[]"', () => { 15 | type test1 = Expect>; 16 | }); 17 | -------------------------------------------------------------------------------- /src/03-type-predicates-assertion-functions/13-assertion-functions.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | interface User { 5 | id: string; 6 | name: string; 7 | } 8 | 9 | interface AdminUser extends User { 10 | role: "admin"; 11 | organisations: string[]; 12 | } 13 | 14 | interface NormalUser extends User { 15 | role: "normal"; 16 | } 17 | 18 | function assertUserIsAdmin(user: NormalUser | AdminUser) { 19 | if (user.role !== "admin") { 20 | throw new Error("Not an admin user"); 21 | } 22 | } 23 | 24 | it("Should throw an error when it encounters a normal user", () => { 25 | const user: NormalUser = { 26 | id: "user_1", 27 | name: "Miles", 28 | role: "normal", 29 | }; 30 | 31 | expect(() => assertUserIsAdmin(user)).toThrow(); 32 | }); 33 | 34 | it("Should assert that the type is an admin user after it has been validated", () => { 35 | const example = (user: NormalUser | AdminUser) => { 36 | assertUserIsAdmin(user); 37 | 38 | type tests = [Expect>]; 39 | }; 40 | }); 41 | -------------------------------------------------------------------------------- /src/03-type-predicates-assertion-functions/13-assertion-functions.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | interface User { 5 | id: string; 6 | name: string; 7 | } 8 | 9 | interface AdminUser extends User { 10 | role: "admin"; 11 | organisations: string[]; 12 | } 13 | 14 | interface NormalUser extends User { 15 | role: "normal"; 16 | } 17 | 18 | function assertUserIsAdmin( 19 | user: NormalUser | AdminUser, 20 | ): asserts user is AdminUser { 21 | if (user.role !== "admin") { 22 | throw new Error("Not an admin user"); 23 | } 24 | } 25 | 26 | it("Should throw an error when it encounters a normal user", () => { 27 | const user: NormalUser = { 28 | id: "user_1", 29 | name: "Miles", 30 | role: "normal", 31 | }; 32 | 33 | expect(() => assertUserIsAdmin(user)).toThrow(); 34 | }); 35 | 36 | it("Should assert that the type is an admin user after it has been validated", () => { 37 | const example = (user: NormalUser | AdminUser) => { 38 | assertUserIsAdmin(user); 39 | 40 | type tests = [Expect>]; 41 | }; 42 | }); 43 | -------------------------------------------------------------------------------- /src/03-type-predicates-assertion-functions/14-typescripts-worst-error.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | interface User { 5 | id: string; 6 | name: string; 7 | } 8 | 9 | interface AdminUser extends User { 10 | role: "admin"; 11 | organisations: string[]; 12 | } 13 | 14 | interface NormalUser extends User { 15 | role: "normal"; 16 | } 17 | 18 | const assertUserIsAdmin = ( 19 | user: NormalUser | AdminUser, 20 | ): asserts user is AdminUser => { 21 | if (user.role !== "admin") { 22 | throw new Error("Not an admin user"); 23 | } 24 | }; 25 | 26 | it("Should throw an error when it encounters a normal user", () => { 27 | const user: NormalUser = { 28 | id: "user_1", 29 | name: "Miles", 30 | role: "normal", 31 | }; 32 | 33 | expect(() => assertUserIsAdmin(user)).toThrow(); 34 | }); 35 | 36 | it("Should assert that the type is an admin user after it has been validated", () => { 37 | const example = (user: NormalUser | AdminUser) => { 38 | /** 39 | * Why is this error happening? 40 | * 41 | * Note: PLEASE DON'T SPEND TOO LONG HERE - feel 42 | * free to use the solution. I have personally wasted 43 | * hours on this error. 44 | */ 45 | assertUserIsAdmin(user); 46 | 47 | type tests = [Expect>]; 48 | }; 49 | }); 50 | -------------------------------------------------------------------------------- /src/03-type-predicates-assertion-functions/14-typescripts-worst-error.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | interface User { 5 | id: string; 6 | name: string; 7 | } 8 | 9 | interface AdminUser extends User { 10 | role: "admin"; 11 | organisations: string[]; 12 | } 13 | 14 | interface NormalUser extends User { 15 | role: "normal"; 16 | } 17 | 18 | function assertUserIsAdmin( 19 | user: NormalUser | AdminUser, 20 | ): asserts user is AdminUser { 21 | if (user.role !== "admin") { 22 | throw new Error("Not an admin user"); 23 | } 24 | } 25 | 26 | it("Should throw an error when it encounters a normal user", () => { 27 | const user: NormalUser = { 28 | id: "user_1", 29 | name: "Miles", 30 | role: "normal", 31 | }; 32 | 33 | expect(() => assertUserIsAdmin(user)).toThrow(); 34 | }); 35 | 36 | it("Should assert that the type is an admin user after it has been validated", () => { 37 | const example = (user: NormalUser | AdminUser) => { 38 | /** 39 | * The fix is to make assertUserIsAdmin a function, 40 | * not an arrow function. Lord above. 41 | */ 42 | assertUserIsAdmin(user); 43 | 44 | type tests = [Expect>]; 45 | }; 46 | }); 47 | -------------------------------------------------------------------------------- /src/03-type-predicates-assertion-functions/15-type-predicates-with-generics.problem.ts: -------------------------------------------------------------------------------- 1 | import { isBodyElement, isDivElement } from "fake-external-lib"; 2 | import { it } from "vitest"; 3 | import { Equal, Expect } from "../helpers/type-utils"; 4 | 5 | /** 6 | * By changing the type definition of this interface, 7 | * you can fix all the errors below. 8 | */ 9 | interface DOMNodeExtractorConfig { 10 | isNode: (node: unknown) => boolean; 11 | transform: (node: T) => Result; 12 | } 13 | 14 | const createDOMNodeExtractor = ( 15 | config: DOMNodeExtractorConfig, 16 | ) => { 17 | return (nodes: unknown[]): TResult[] => { 18 | return nodes.filter(config.isNode).map(config.transform); 19 | }; 20 | }; 21 | 22 | it('Should pick up that "extractDivs" is of type "HTMLDivElement[]"', () => { 23 | const extractDivs = createDOMNodeExtractor({ 24 | isNode: isDivElement, 25 | transform: (div) => { 26 | type test1 = Expect>; 27 | return div.innerText; 28 | }, 29 | }); 30 | 31 | const divs = extractDivs([document.createElement("div")]); 32 | 33 | type test2 = Expect>; 34 | }); 35 | 36 | it('Should pick up that "extractBodies" is of type "HTMLBodyElement[]"', () => { 37 | const extractBodies = createDOMNodeExtractor({ 38 | isNode: isBodyElement, 39 | transform: (body) => { 40 | type test1 = Expect>; 41 | 42 | return body.bgColor; 43 | }, 44 | }); 45 | 46 | const bodies = extractBodies([document.createElement("body")]); 47 | 48 | type test2 = Expect>; 49 | }); 50 | -------------------------------------------------------------------------------- /src/03-type-predicates-assertion-functions/15-type-predicates-with-generics.solution.ts: -------------------------------------------------------------------------------- 1 | import { isBodyElement, isDivElement } from "fake-external-lib"; 2 | import { it } from "vitest"; 3 | import { Equal, Expect } from "../helpers/type-utils"; 4 | 5 | interface DOMNodeExtractorConfig { 6 | /** 7 | * Here, node is T lets you specify that 8 | * isNode takes in a type predicate. 9 | */ 10 | isNode: (node: unknown) => node is T; 11 | transform: (node: T) => Result; 12 | } 13 | 14 | const createDOMNodeExtractor = ( 15 | config: DOMNodeExtractorConfig, 16 | ) => { 17 | return (nodes: unknown[]): TResult[] => { 18 | return nodes.filter(config.isNode).map(config.transform); 19 | }; 20 | }; 21 | 22 | it('Should pick up that "extractDivs" is of type "HTMLDivElement[]"', () => { 23 | const extractDivs = createDOMNodeExtractor({ 24 | isNode: isDivElement, 25 | transform: (div) => { 26 | type test1 = Expect>; 27 | return div.innerText; 28 | }, 29 | }); 30 | 31 | const divs = extractDivs([document.createElement("div")]); 32 | 33 | type test2 = Expect>; 34 | }); 35 | 36 | it('Should pick up that "extractBodies" is of type "HTMLBodyElement[]"', () => { 37 | const extractBodies = createDOMNodeExtractor({ 38 | isNode: isBodyElement, 39 | transform: (body) => { 40 | type test1 = Expect>; 41 | 42 | return body.bgColor; 43 | }, 44 | }); 45 | 46 | const bodies = extractBodies([document.createElement("body")]); 47 | 48 | type test2 = Expect>; 49 | }); 50 | -------------------------------------------------------------------------------- /src/03-type-predicates-assertion-functions/16-brands-and-type-predicates.problem.ts: -------------------------------------------------------------------------------- 1 | import { it } from "vitest"; 2 | import { Brand } from "../helpers/Brand"; 3 | 4 | type Valid = Brand; 5 | 6 | interface PasswordValues { 7 | password: string; 8 | confirmPassword: string; 9 | } 10 | 11 | /** 12 | * 💡 You'll need to change this function... 13 | */ 14 | const isValidPassword = (values: PasswordValues) => { 15 | if (values.password !== values.confirmPassword) { 16 | return false; 17 | } 18 | return true; 19 | }; 20 | 21 | const createUserOnApi = (values: Valid) => { 22 | // Imagine this function creates the user on the API 23 | }; 24 | 25 | it("Should fail if you do not validate the values before calling createUserOnApi", () => { 26 | const onSubmitHandler = (values: PasswordValues) => { 27 | // @ts-expect-error 28 | createUserOnApi(values); 29 | }; 30 | }); 31 | 32 | it("Should succeed if you DO validate the values before calling createUserOnApi", () => { 33 | const onSubmitHandler = (values: PasswordValues) => { 34 | if (isValidPassword(values)) { 35 | createUserOnApi(values); 36 | } 37 | }; 38 | }); 39 | -------------------------------------------------------------------------------- /src/03-type-predicates-assertion-functions/16-brands-and-type-predicates.solution.ts: -------------------------------------------------------------------------------- 1 | import { it } from "vitest"; 2 | import { Brand } from "../helpers/Brand"; 3 | 4 | type Valid = Brand; 5 | 6 | interface PasswordValues { 7 | password: string; 8 | confirmPassword: string; 9 | } 10 | 11 | const isValidPassword = ( 12 | values: PasswordValues, 13 | ): values is Valid => { 14 | if (values.password !== values.confirmPassword) { 15 | return false; 16 | } 17 | return true; 18 | }; 19 | 20 | const createUserOnApi = (values: Valid) => { 21 | // Imagine this function creates the user on the API 22 | }; 23 | 24 | it("Should fail if you do not validate the passwords before calling createUserOnApi", () => { 25 | const onSubmitHandler = (values: PasswordValues) => { 26 | // @ts-expect-error 27 | createUserOnApi(values); 28 | }; 29 | }); 30 | 31 | it("Should succeed if you DO validate the passwords before calling createUserOnApi", () => { 32 | const onSubmitHandler = (values: PasswordValues) => { 33 | if (isValidPassword(values)) { 34 | createUserOnApi(values); 35 | } 36 | }; 37 | }); 38 | -------------------------------------------------------------------------------- /src/03-type-predicates-assertion-functions/17-brands-and-assertion-functions.problem.ts: -------------------------------------------------------------------------------- 1 | import { it } from "vitest"; 2 | import { Brand } from "../helpers/Brand"; 3 | 4 | type Valid = Brand; 5 | 6 | interface PasswordValues { 7 | password: string; 8 | confirmPassword: string; 9 | } 10 | 11 | /** 12 | * 💡 You'll need to change this function... 13 | */ 14 | function assertIsValidPassword(values: PasswordValues) { 15 | if (values.password !== values.confirmPassword) { 16 | throw new Error("Password is invalid"); 17 | } 18 | } 19 | 20 | const createUserOnApi = (values: Valid) => { 21 | // Imagine this function creates the user on the API 22 | }; 23 | 24 | it("Should fail if you do not validate the passwords before calling createUserOnApi", () => { 25 | const onSubmitHandler = (values: PasswordValues) => { 26 | // @ts-expect-error 27 | createUserOnApi(values); 28 | }; 29 | }); 30 | 31 | it("Should succeed if you DO validate the passwords before calling createUserOnApi", () => { 32 | const onSubmitHandler = (values: PasswordValues) => { 33 | assertIsValidPassword(values); 34 | createUserOnApi(values); 35 | }; 36 | }); 37 | -------------------------------------------------------------------------------- /src/03-type-predicates-assertion-functions/17-brands-and-assertion-functions.solution.ts: -------------------------------------------------------------------------------- 1 | import { it } from "vitest"; 2 | import { Brand } from "../helpers/Brand"; 3 | 4 | type Valid = Brand; 5 | 6 | interface PasswordValues { 7 | password: string; 8 | confirmPassword: string; 9 | } 10 | 11 | function assertIsValidPassword( 12 | values: PasswordValues, 13 | ): asserts values is Valid { 14 | if (values.password !== values.confirmPassword) { 15 | throw new Error("Password is invalid"); 16 | } 17 | } 18 | 19 | const createUserOnApi = (values: Valid) => { 20 | // Imagine this function creates the user on the API 21 | }; 22 | 23 | it("Should fail if you do not validate the passwords before calling createUserOnApi", () => { 24 | const onSubmitHandler = (values: PasswordValues) => { 25 | // @ts-expect-error 26 | createUserOnApi(values); 27 | }; 28 | }); 29 | 30 | it("Should succeed if you DO validate the passwords before calling createUserOnApi", () => { 31 | const onSubmitHandler = (values: PasswordValues) => { 32 | assertIsValidPassword(values); 33 | createUserOnApi(values); 34 | }; 35 | }); 36 | -------------------------------------------------------------------------------- /src/04-classes/17.8-classes-as-types-and-value.problem.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "../helpers/type-utils"; 2 | 3 | class CustomError extends Error { 4 | constructor(message: string, public code: number) { 5 | super(message); 6 | this.name = "CustomError"; 7 | } 8 | } 9 | 10 | // How do we type the 'error' parameter? 11 | const handleCustomError = (error: unknown) => { 12 | console.error(error.code); 13 | 14 | type test = Expect>; 15 | }; 16 | 17 | export {}; 18 | -------------------------------------------------------------------------------- /src/04-classes/17.8-classes-as-types-and-value.solution.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "../helpers/type-utils"; 2 | 3 | class CustomError extends Error { 4 | constructor(message: string, public code: number) { 5 | super(message); 6 | this.name = "CustomError"; 7 | } 8 | } 9 | 10 | const handleCustomError = (error: CustomError) => { 11 | console.error(error.code); 12 | 13 | type test = Expect>; 14 | }; 15 | 16 | export {}; 17 | -------------------------------------------------------------------------------- /src/04-classes/18-type-predicates-and-classes.problem.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "../helpers/type-utils"; 2 | 3 | class Form { 4 | error?: string; 5 | 6 | constructor( 7 | public values: TValues, 8 | private validate: (values: TValues) => string | void, 9 | ) {} 10 | 11 | isInvalid() { 12 | const result = this.validate(this.values); 13 | 14 | if (typeof result === "string") { 15 | this.error = result; 16 | return true; 17 | } 18 | 19 | this.error = undefined; 20 | return false; 21 | } 22 | } 23 | 24 | const form = new Form( 25 | { 26 | username: "", 27 | password: "", 28 | }, 29 | (values) => { 30 | if (!values.username) { 31 | return "Username is required"; 32 | } 33 | 34 | if (!values.password) { 35 | return "Password is required"; 36 | } 37 | }, 38 | ); 39 | 40 | if (form.isInvalid()) { 41 | type test1 = Expect>; 42 | } else { 43 | type test2 = Expect>; 44 | } 45 | -------------------------------------------------------------------------------- /src/04-classes/18-type-predicates-and-classes.solution.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "../helpers/type-utils"; 2 | 3 | class Form { 4 | error?: string; 5 | 6 | constructor( 7 | public values: TValues, 8 | private validate: (values: TValues) => string | void, 9 | ) {} 10 | 11 | isInvalid(): this is this & { error: string } { 12 | const result = this.validate(this.values); 13 | 14 | if (typeof result === "string") { 15 | this.error = result; 16 | return true; 17 | } 18 | 19 | this.error = undefined; 20 | return false; 21 | } 22 | } 23 | 24 | const form = new Form( 25 | { 26 | username: "", 27 | password: "", 28 | }, 29 | (values) => { 30 | if (!values.username) { 31 | return "Username is required"; 32 | } 33 | 34 | if (!values.password) { 35 | return "Password is required"; 36 | } 37 | }, 38 | ); 39 | 40 | if (form.isInvalid()) { 41 | type test1 = Expect>; 42 | } else { 43 | type test2 = Expect>; 44 | } 45 | -------------------------------------------------------------------------------- /src/04-classes/18.2-assertion-functions-and-classes.problem.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "../helpers/type-utils"; 2 | 3 | interface User { 4 | id: string; 5 | } 6 | 7 | export class SDK { 8 | loggedInUser?: User; 9 | 10 | constructor(loggedInUser?: User) { 11 | this.loggedInUser = loggedInUser; 12 | } 13 | 14 | // How do we type this assertion function? 15 | assertIsLoggedIn() { 16 | if (!this.loggedInUser) { 17 | throw new Error("Not logged in"); 18 | } 19 | } 20 | 21 | createPost(title: string, body: string) { 22 | type test1 = Expect>; 23 | 24 | this.assertIsLoggedIn(); 25 | 26 | type test2 = Expect>; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/04-classes/18.2-assertion-functions-and-classes.solution.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "../helpers/type-utils"; 2 | 3 | interface User { 4 | id: string; 5 | } 6 | 7 | export class SDK { 8 | loggedInUser?: User; 9 | 10 | constructor(loggedInUser?: User) { 11 | this.loggedInUser = loggedInUser; 12 | } 13 | 14 | assertIsLoggedIn(): asserts this is this & { loggedInUser: User } { 15 | if (!this.loggedInUser) { 16 | throw new Error("Not logged in"); 17 | } 18 | } 19 | 20 | createPost(title: string, body: string) { 21 | type test1 = Expect>; 22 | 23 | this.assertIsLoggedIn(); 24 | 25 | type test2 = Expect>; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/04-classes/19.3-builder-pattern-intro.explainer.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "../helpers/type-utils"; 2 | 3 | export class BuilderTuple { 4 | list: TList; 5 | 6 | constructor() { 7 | this.list = [] as any; 8 | } 9 | 10 | push(num: TNum): BuilderTuple<[...TList, TNum]> { 11 | this.list.push(num); 12 | 13 | return this as any; 14 | } 15 | 16 | unshift(num: TNum): BuilderTuple<[TNum, ...TList]> { 17 | this.list.unshift(num); 18 | 19 | return this as any; 20 | } 21 | } 22 | 23 | const builderBeforePush = new BuilderTuple(); 24 | const listBeforePush = builderBeforePush.list; 25 | 26 | const builderAfterPush = builderBeforePush.unshift(3).unshift(2).unshift(1); 27 | const listAfterPush = builderAfterPush.list; 28 | 29 | type tests = [ 30 | Expect>>, 31 | Expect>, 32 | Expect>>, 33 | Expect> 34 | ]; 35 | -------------------------------------------------------------------------------- /src/04-classes/19.4-alex-trpc-builder-pattern.explainer.ts: -------------------------------------------------------------------------------- 1 | // Alex from tRPC explains the builder pattern 2 | 3 | export {}; 4 | -------------------------------------------------------------------------------- /src/04-classes/20-type-safe-map.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | /** 4 | * In this problem, we need to type the return type of the set() 5 | * method to make it add keys to the TMap generic. 6 | * 7 | * In the return type of set(), we'll need to modify the TMap 8 | * generic to add the new key/value pair. 9 | */ 10 | 11 | class TypeSafeStringMap = {}> { 12 | private map: TMap; 13 | constructor() { 14 | this.map = {} as TMap; 15 | } 16 | 17 | get(key: keyof TMap): string { 18 | return this.map[key]; 19 | } 20 | 21 | set(key: K, value: string): unknown { 22 | (this.map[key] as any) = value; 23 | 24 | return this; 25 | } 26 | } 27 | 28 | const map = new TypeSafeStringMap() 29 | .set("matt", "pocock") 30 | .set("jools", "holland") 31 | .set("brandi", "carlile"); 32 | 33 | it("Should not allow getting values which do not exist", () => { 34 | map.get( 35 | // @ts-expect-error 36 | "jim", 37 | ); 38 | }); 39 | 40 | it("Should return values from keys which do exist", () => { 41 | expect(map.get("matt")).toBe("pocock"); 42 | expect(map.get("jools")).toBe("holland"); 43 | expect(map.get("brandi")).toBe("carlile"); 44 | }); 45 | -------------------------------------------------------------------------------- /src/04-classes/20-type-safe-map.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | class TypeSafeStringMap = {}> { 4 | private map: TMap; 5 | constructor() { 6 | this.map = {} as TMap; 7 | } 8 | 9 | get(key: keyof TMap): string { 10 | return this.map[key]; 11 | } 12 | 13 | set( 14 | key: K, 15 | value: string, 16 | ): TypeSafeStringMap> { 17 | (this.map[key] as any) = value; 18 | 19 | return this; 20 | } 21 | } 22 | 23 | const map = new TypeSafeStringMap() 24 | .set("matt", "pocock") 25 | .set("jools", "holland") 26 | .set("brandi", "carlile"); 27 | 28 | it("Should not allow getting values which do not exist", () => { 29 | map.get( 30 | // @ts-expect-error 31 | "jim", 32 | ); 33 | }); 34 | 35 | it("Should return values from keys which do exist", () => { 36 | expect(map.get("matt")).toBe("pocock"); 37 | expect(map.get("jools")).toBe("holland"); 38 | expect(map.get("brandi")).toBe("carlile"); 39 | }); 40 | -------------------------------------------------------------------------------- /src/04-classes/21-importance-of-default-generic.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | /** 4 | * I've made a small change to the solution of the previous problem 5 | * which breaks it. Can you spot what it is? 6 | * 7 | * Clue: it's somewhere inside class TypeSafeStringMap, and it's 8 | * on the type level - not the runtime level. 9 | */ 10 | class TypeSafeStringMap> { 11 | private map: TMap; 12 | constructor() { 13 | this.map = {} as TMap; 14 | } 15 | 16 | get(key: keyof TMap): string { 17 | return this.map[key]; 18 | } 19 | 20 | set( 21 | key: K, 22 | value: string, 23 | ): TypeSafeStringMap> { 24 | (this.map[key] as any) = value; 25 | 26 | return this; 27 | } 28 | } 29 | 30 | const map = new TypeSafeStringMap() 31 | .set("matt", "pocock") 32 | .set("jools", "holland") 33 | .set("brandi", "carlile"); 34 | 35 | it("Should not allow getting values which do not exist", () => { 36 | map.get( 37 | // @ts-expect-error 38 | "jim", 39 | ); 40 | }); 41 | 42 | it("Should return values from keys which do exist", () => { 43 | expect(map.get("matt")).toBe("pocock"); 44 | expect(map.get("jools")).toBe("holland"); 45 | expect(map.get("brandi")).toBe("carlile"); 46 | }); 47 | -------------------------------------------------------------------------------- /src/04-classes/21-importance-of-default-generic.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | class TypeSafeStringMap = {}> { 4 | private map: TMap; 5 | constructor() { 6 | this.map = {} as TMap; 7 | } 8 | 9 | get(key: keyof TMap): string { 10 | return this.map[key]; 11 | } 12 | 13 | set( 14 | key: K, 15 | value: string 16 | ): TypeSafeStringMap> { 17 | (this.map[key] as any) = value; 18 | 19 | return this; 20 | } 21 | } 22 | 23 | const map = new TypeSafeStringMap() 24 | .set("matt", "pocock") 25 | .set("jools", "holland") 26 | .set("brandi", "carlile"); 27 | 28 | it("Should not allow getting values which do not exist", () => { 29 | map.get( 30 | // @ts-expect-error 31 | "jim" 32 | ); 33 | }); 34 | 35 | it("Should return values from keys which do exist", () => { 36 | expect(map.get("matt")).toBe("pocock"); 37 | expect(map.get("jools")).toBe("holland"); 38 | expect(map.get("brandi")).toBe("carlile"); 39 | }); 40 | -------------------------------------------------------------------------------- /src/04-classes/22-dynamic-middleware.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { fetchUser } from "fake-external-lib"; 3 | 4 | type Middleware = ( 5 | input: TInput 6 | ) => TOutput | Promise; 7 | 8 | /** 9 | * In this problem, we need to type the return type of the use() 10 | * method to make it update the TOutput generic with a new one. 11 | * 12 | * Currently, the use method just uses the same TOutput as the 13 | * first middleware you pass in. But it should infer the _new_ 14 | * output from the middleware you pass in. 15 | */ 16 | class DynamicMiddleware { 17 | private middleware: Middleware[] = []; 18 | 19 | constructor(firstMiddleware: Middleware) { 20 | this.middleware.push(firstMiddleware); 21 | } 22 | 23 | // Clue: you'll need to make changes here! 24 | use(middleware: Middleware): unknown { 25 | this.middleware.push(middleware); 26 | 27 | return this as any; 28 | // ^ You'll need the 'as any'! 29 | } 30 | 31 | async run(input: TInput): Promise { 32 | let result: TOutput = input as any; 33 | 34 | for (const middleware of this.middleware) { 35 | result = await middleware(result); 36 | } 37 | 38 | return result; 39 | } 40 | } 41 | 42 | const middleware = new DynamicMiddleware((req: Request) => { 43 | return { 44 | ...req, 45 | // Transforms /user/123 to 123 46 | userId: req.url.split("/")[2], 47 | }; 48 | }) 49 | .use((req) => { 50 | if (req.userId === "123") { 51 | throw new Error(); 52 | } 53 | return req; 54 | }) 55 | .use(async (req) => { 56 | return { 57 | ...req, 58 | user: await fetchUser(req.userId), 59 | }; 60 | }); 61 | 62 | it("Should fail if the user id is 123", () => { 63 | expect(middleware.run({ url: "/user/123" } as Request)).rejects.toThrow(); 64 | }); 65 | 66 | it("Should return a request with a user", async () => { 67 | const result = await middleware.run({ url: "/user/matt" } as Request); 68 | 69 | expect(result.user.id).toBe("matt"); 70 | expect(result.user.firstName).toBe("John"); 71 | expect(result.user.lastName).toBe("Doe"); 72 | }); 73 | -------------------------------------------------------------------------------- /src/04-classes/22-dynamic-middleware.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { fetchUser } from "fake-external-lib"; 3 | 4 | type Middleware = ( 5 | input: TInput 6 | ) => TOutput | Promise; 7 | 8 | /** 9 | * In this problem, we need to type the return type of the use() 10 | * method to make it update the TOutput generic with a new one. 11 | */ 12 | class DynamicMiddleware { 13 | private middleware: Middleware[] = []; 14 | 15 | constructor(firstMiddleware: Middleware) { 16 | this.middleware.push(firstMiddleware); 17 | } 18 | 19 | use( 20 | middleware: Middleware 21 | ): DynamicMiddleware { 22 | this.middleware.push(middleware); 23 | 24 | return this as any; 25 | } 26 | 27 | async run(input: TInput): Promise { 28 | let result: TOutput = input as any; 29 | 30 | for (const middleware of this.middleware) { 31 | result = await middleware(result); 32 | } 33 | 34 | return result; 35 | } 36 | } 37 | 38 | const middleware = new DynamicMiddleware((req: Request) => { 39 | return { 40 | ...req, 41 | // Transforms /user/123 to 123 42 | userId: req.url.split("/")[2], 43 | }; 44 | }) 45 | .use((req) => { 46 | if (req.userId === "123") { 47 | throw new Error(); 48 | } 49 | return req; 50 | }) 51 | .use(async (req) => { 52 | return { 53 | ...req, 54 | user: await fetchUser(req.userId), 55 | }; 56 | }); 57 | 58 | it("Should fail if the user id is 123", () => { 59 | expect(middleware.run({ url: "/user/123" } as Request)).rejects.toThrow(); 60 | }); 61 | 62 | it("Should return a request with a user", async () => { 63 | const result = await middleware.run({ url: "/user/matt" } as Request); 64 | 65 | expect(result.user.id).toBe("matt"); 66 | expect(result.user.firstName).toBe("John"); 67 | expect(result.user.lastName).toBe("Doe"); 68 | }); 69 | -------------------------------------------------------------------------------- /src/04-classes/22.5-subclassing-in-zod.explainer.ts: -------------------------------------------------------------------------------- 1 | // Colin from Zod explains how using subclasses can make generics 2 | // look nice in intellisense. 3 | 4 | type ZodString = { 5 | min: () => any; 6 | max: () => any; 7 | } & ZodType; 8 | 9 | type ZodType = { 10 | parse: (input: unknown) => T; 11 | }; 12 | 13 | const str: ZodString = {} as any; 14 | 15 | export {}; 16 | -------------------------------------------------------------------------------- /src/05-external-libraries/22.9-where-do-external-types-come-from.explainer.ts: -------------------------------------------------------------------------------- 1 | // Orta explains where external types come from 2 | 3 | export {}; 4 | -------------------------------------------------------------------------------- /src/05-external-libraries/23-extract-external-lib-types.problem.ts: -------------------------------------------------------------------------------- 1 | import { fetchUser } from "fake-external-lib"; 2 | import { Equal, Expect, ExpectExtends } from "../helpers/type-utils"; 3 | 4 | /** 5 | * We're using a function from fake-external lib, but we need 6 | * to extend the types. Extract the types below. 7 | */ 8 | 9 | type ParametersOfFetchUser = unknown; 10 | 11 | type ReturnTypeOfFetchUserWithFullName = unknown; 12 | 13 | export const fetchUserWithFullName = async ( 14 | ...args: ParametersOfFetchUser 15 | ): Promise => { 16 | const user = await fetchUser(...args); 17 | return { 18 | ...user, 19 | fullName: `${user.firstName} ${user.lastName}`, 20 | }; 21 | }; 22 | 23 | type tests = [ 24 | Expect>, 25 | Expect< 26 | ExpectExtends< 27 | { id: string; firstName: string; lastName: string; fullName: string }, 28 | ReturnTypeOfFetchUserWithFullName 29 | > 30 | >, 31 | ]; 32 | -------------------------------------------------------------------------------- /src/05-external-libraries/23-extract-external-lib-types.solution.ts: -------------------------------------------------------------------------------- 1 | import { fetchUser } from "fake-external-lib"; 2 | import { Equal, Expect, ExpectExtends } from "../helpers/type-utils"; 3 | 4 | type ParametersOfFetchUser = Parameters; 5 | 6 | type ReturnTypeOfFetchUserWithFullName = Awaited< 7 | ReturnType 8 | > & { fullName: string }; 9 | 10 | export const fetchUserWithFullName = async ( 11 | ...args: ParametersOfFetchUser 12 | ): Promise => { 13 | const user = await fetchUser(...args); 14 | return { 15 | ...user, 16 | fullName: `${user.firstName} ${user.lastName}`, 17 | }; 18 | }; 19 | 20 | type tests = [ 21 | Expect>, 22 | Expect< 23 | ExpectExtends< 24 | ReturnTypeOfFetchUserWithFullName, 25 | { id: string; firstName: string; lastName: string; fullName: string } 26 | > 27 | >, 28 | ]; 29 | -------------------------------------------------------------------------------- /src/05-external-libraries/23.9-lodash-types.explainer.ts: -------------------------------------------------------------------------------- 1 | // Explain lodash's types 2 | 3 | export {}; 4 | -------------------------------------------------------------------------------- /src/05-external-libraries/24-lodash-groupby.problem.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { expect, it } from "vitest"; 3 | import { doNotExecute, Equal, Expect } from "../helpers/type-utils"; 4 | 5 | /** 6 | * We've made a reusable function here to group 7 | * arrays of objects by age. I want you to: 8 | * 9 | * 1. Make sure that the errors (below) disappear 10 | * 2. Take a look at the typings for _.groupBy to 11 | * see if you can understand them. 12 | */ 13 | const groupByAge = (array: unknown[]) => { 14 | const grouped = _.groupBy(array, "age"); 15 | 16 | return grouped; 17 | }; 18 | 19 | const result = groupByAge([ 20 | { 21 | name: "John", 22 | age: 20, 23 | }, 24 | { 25 | name: "Jane", 26 | age: 20, 27 | }, 28 | { 29 | name: "Mary", 30 | age: 30, 31 | }, 32 | ]); 33 | 34 | it("Should group the items by age", () => { 35 | expect(result).toEqual({ 36 | 20: [ 37 | { 38 | name: "John", 39 | age: 20, 40 | }, 41 | { 42 | name: "Jane", 43 | age: 20, 44 | }, 45 | ], 46 | 30: [ 47 | { 48 | name: "Mary", 49 | age: 30, 50 | }, 51 | ], 52 | }); 53 | 54 | type tests = [ 55 | Expect>> 56 | ]; 57 | }); 58 | 59 | it("Should not let you pass in an array of objects NOT containing age", () => { 60 | doNotExecute(() => { 61 | groupByAge([ 62 | { 63 | // @ts-expect-error 64 | name: "John", 65 | }, 66 | { 67 | // @ts-expect-error 68 | name: "Bill", 69 | }, 70 | ]); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/05-external-libraries/24-lodash-groupby.solution.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { expect, it } from "vitest"; 3 | import { doNotExecute, Equal, Expect } from "../helpers/type-utils"; 4 | 5 | const groupByAge = (array: T[]) => { 6 | const grouped = _.groupBy(array, "age"); 7 | 8 | return grouped; 9 | }; 10 | 11 | const result = groupByAge([ 12 | { 13 | name: "John", 14 | age: 20, 15 | }, 16 | { 17 | name: "Jane", 18 | age: 20, 19 | }, 20 | { 21 | name: "Mary", 22 | age: 30, 23 | }, 24 | ]); 25 | 26 | it("Should group the items by age", () => { 27 | expect(result).toEqual({ 28 | 20: [ 29 | { 30 | name: "John", 31 | age: 20, 32 | }, 33 | { 34 | name: "Jane", 35 | age: 20, 36 | }, 37 | ], 38 | 30: [ 39 | { 40 | name: "Mary", 41 | age: 30, 42 | }, 43 | ], 44 | }); 45 | 46 | type tests = [ 47 | Expect>>, 48 | ]; 49 | }); 50 | 51 | it("Should not let you pass in an array of objects NOT containing age", () => { 52 | doNotExecute(() => { 53 | groupByAge([ 54 | { 55 | // @ts-expect-error 56 | name: "John", 57 | }, 58 | { 59 | // @ts-expect-error 60 | name: "Bill", 61 | }, 62 | ]); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/05-external-libraries/24.9-express-types.explainer.ts: -------------------------------------------------------------------------------- 1 | import express, { RequestHandler } from "express"; 2 | 3 | const app = express(); 4 | 5 | // /user?id=124123 6 | const getUser: RequestHandler< 7 | any, 8 | { 9 | name: string; 10 | }, 11 | any, 12 | { 13 | id: string; 14 | } 15 | > = (req, res) => { 16 | req.query.id; 17 | }; 18 | 19 | app.get("/user"); 20 | 21 | app.post("/user", (req, res) => {}); 22 | 23 | app.listen(3000); 24 | -------------------------------------------------------------------------------- /src/05-external-libraries/25-usage-with-express.problem.ts: -------------------------------------------------------------------------------- 1 | import express, { 2 | NextFunction, 3 | Request, 4 | RequestHandler, 5 | Response, 6 | } from "express"; 7 | import { Equal, Expect } from "../helpers/type-utils"; 8 | 9 | const app = express(); 10 | 11 | const makeTypeSafeGet = 12 | ( 13 | parser: (queryParams: Request["query"]) => unknown, 14 | handler: RequestHandler 15 | ) => 16 | (req: Request, res: Response, next: NextFunction) => { 17 | try { 18 | parser(req.query); 19 | } catch (e) { 20 | res.status(400).send("Invalid query: " + (e as Error).message); 21 | return; 22 | } 23 | 24 | return handler(req, res, next); 25 | }; 26 | 27 | const getUser = makeTypeSafeGet( 28 | (query) => { 29 | if (typeof query.id !== "string") { 30 | throw new Error("You must pass an id"); 31 | } 32 | 33 | return { 34 | id: query.id, 35 | }; 36 | }, 37 | (req, res) => { 38 | // req.query should be EXACTLY the type returned from 39 | // the parser above 40 | type tests = [Expect>]; 41 | 42 | res.json({ 43 | id: req.query.id, 44 | name: "Matt", 45 | }); 46 | } 47 | ); 48 | 49 | app.get("/user", getUser); 50 | -------------------------------------------------------------------------------- /src/05-external-libraries/25-usage-with-express.solution.ts: -------------------------------------------------------------------------------- 1 | import express, { 2 | NextFunction, 3 | Request, 4 | RequestHandler, 5 | Response, 6 | } from "express"; 7 | import { Equal, Expect } from "../helpers/type-utils"; 8 | 9 | const app = express(); 10 | 11 | const makeTypeSafeGet = 12 | ( 13 | parser: (queryParams: Request["query"]) => TQuery, 14 | handler: RequestHandler 15 | ) => 16 | (req: Request, res: Response, next: NextFunction) => { 17 | try { 18 | parser(req.query); 19 | } catch (e) { 20 | res.status(400).send("Invalid query: " + (e as Error).message); 21 | return; 22 | } 23 | 24 | return handler(req, res, next); 25 | }; 26 | 27 | const getUser = makeTypeSafeGet( 28 | (query) => { 29 | if (typeof query.id !== "string") { 30 | throw new Error("You must pass an id"); 31 | } 32 | 33 | return { 34 | id: query.id, 35 | }; 36 | }, 37 | (req, res) => { 38 | // req.query should be EXACTLY the type returned from 39 | // the parser above 40 | type tests = [Expect>]; 41 | 42 | res.json({ 43 | id: req.query.id, 44 | name: "Matt", 45 | }); 46 | } 47 | ); 48 | 49 | app.get("/user", getUser); 50 | -------------------------------------------------------------------------------- /src/05-external-libraries/25.9-zod-types.explainer.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | type Example = z.ZodType; 4 | 5 | const transformer = z.string().transform((s) => Number(s)); 6 | -------------------------------------------------------------------------------- /src/05-external-libraries/26-usage-with-zod.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { z } from "zod"; 3 | 4 | const makeZodSafeFunction = ( 5 | schema: unknown, 6 | func: (arg: unknown) => unknown 7 | ) => { 8 | return (arg: unknown) => { 9 | const result = schema.parse(arg); 10 | return func(result); 11 | }; 12 | }; 13 | 14 | const addTwoNumbersArg = z.object({ 15 | a: z.number(), 16 | b: z.number(), 17 | }); 18 | 19 | const addTwoNumbers = makeZodSafeFunction( 20 | addTwoNumbersArg, 21 | (args) => args.a + args.b 22 | ); 23 | 24 | it("Should error on the type level AND the runtime if you pass incorrect params", () => { 25 | expect(() => 26 | addTwoNumbers( 27 | // @ts-expect-error 28 | { a: 1, badParam: 3 } 29 | ) 30 | ).toThrow(); 31 | }); 32 | 33 | it("Should succeed if you pass the correct type", () => { 34 | expect(addTwoNumbers({ a: 1, b: 2 })).toBe(3); 35 | }); 36 | -------------------------------------------------------------------------------- /src/05-external-libraries/26-usage-with-zod.solution.1.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { z } from "zod"; 3 | 4 | const makeZodSafeFunction = ( 5 | schema: z.Schema, 6 | func: (arg: TValue) => TResult 7 | ) => { 8 | return (arg: TValue) => { 9 | const result = schema.parse(arg); 10 | return func(result); 11 | }; 12 | }; 13 | 14 | const addTwoNumbersArg = z.object({ 15 | a: z.number(), 16 | b: z.number(), 17 | }); 18 | 19 | const addTwoNumbers = makeZodSafeFunction( 20 | addTwoNumbersArg, 21 | (args) => args.a + args.b 22 | ); 23 | 24 | it("Should error on the type level AND the runtime if you pass incorrect params", () => { 25 | expect(() => 26 | addTwoNumbers( 27 | // @ts-expect-error 28 | { a: 1, badParam: 3 } 29 | ) 30 | ).toThrow(); 31 | }); 32 | 33 | it("Should succeed if you pass the correct type", () => { 34 | expect(addTwoNumbers({ a: 1, b: 2 })).toBe(3); 35 | }); 36 | -------------------------------------------------------------------------------- /src/05-external-libraries/26-usage-with-zod.solution.2.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { z } from "zod"; 3 | 4 | const makeZodSafeFunction = ( 5 | schema: z.ZodType, 6 | func: (arg: TValue) => TResult, 7 | ) => { 8 | return (arg: TValue) => { 9 | const result = schema.parse(arg); 10 | return func(result); 11 | }; 12 | }; 13 | 14 | const addTwoNumbersArg = z.object({ 15 | a: z.number(), 16 | b: z.number(), 17 | }); 18 | 19 | const addTwoNumbers = makeZodSafeFunction( 20 | addTwoNumbersArg, 21 | (args) => args.a + args.b, 22 | ); 23 | 24 | it("Should error on the type level AND the runtime if you pass incorrect params", () => { 25 | expect(() => 26 | addTwoNumbers( 27 | // @ts-expect-error 28 | { a: 1, badParam: 3 }, 29 | ), 30 | ).toThrow(); 31 | }); 32 | 33 | it("Should succeed if you pass the correct type", () => { 34 | expect(addTwoNumbers({ a: 1, b: 2 })).toBe(3); 35 | }); 36 | -------------------------------------------------------------------------------- /src/05-external-libraries/26.5-declaration.solution.d.ts: -------------------------------------------------------------------------------- 1 | declare module "fake-animation-lib-solution" { 2 | export type AnimatingState = 3 | | "before-animation" 4 | | "animating" 5 | | "after-animation"; 6 | export function getAnimatingState(): AnimatingState; 7 | } 8 | -------------------------------------------------------------------------------- /src/05-external-libraries/26.5-override-external-lib-types.problem.ts: -------------------------------------------------------------------------------- 1 | import { getAnimatingState } from "fake-animation-lib"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | const animatingState = getAnimatingState(); 5 | 6 | type tests = [ 7 | Expect< 8 | Equal< 9 | typeof animatingState, 10 | "before-animation" | "animating" | "after-animation" 11 | > 12 | > 13 | ]; 14 | -------------------------------------------------------------------------------- /src/05-external-libraries/26.5-override-external-lib-types.solution.ts: -------------------------------------------------------------------------------- 1 | import { getAnimatingState } from "fake-animation-lib-solution"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | const animatingState = getAnimatingState(); 5 | 6 | type tests = [ 7 | Expect< 8 | Equal< 9 | typeof animatingState, 10 | "before-animation" | "animating" | "after-animation" 11 | > 12 | > 13 | ]; 14 | -------------------------------------------------------------------------------- /src/06-identity-functions/27-const-annotations.problem.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "../helpers/type-utils"; 2 | 3 | export const asConst = (t: T) => t; 4 | 5 | const fruits = asConst([ 6 | { 7 | name: "apple", 8 | price: 1, 9 | }, 10 | { 11 | name: "banana", 12 | price: 2, 13 | }, 14 | ]); 15 | 16 | type tests = [ 17 | Expect< 18 | Equal< 19 | typeof fruits, 20 | readonly [ 21 | { 22 | readonly name: "apple"; 23 | readonly price: 1; 24 | }, 25 | { 26 | readonly name: "banana"; 27 | readonly price: 2; 28 | } 29 | ] 30 | > 31 | > 32 | ]; 33 | -------------------------------------------------------------------------------- /src/06-identity-functions/27-const-annotations.solution.1.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "../helpers/type-utils"; 2 | 3 | export const asConst = (t: T) => t; 4 | 5 | const fruits = asConst([ 6 | { 7 | name: "apple", 8 | price: 1, 9 | }, 10 | { 11 | name: "banana", 12 | price: 2, 13 | }, 14 | ]); 15 | 16 | type tests = [ 17 | Expect< 18 | Equal< 19 | typeof fruits, 20 | readonly [ 21 | { 22 | readonly name: "apple"; 23 | readonly price: 1; 24 | }, 25 | { 26 | readonly name: "banana"; 27 | readonly price: 2; 28 | } 29 | ] 30 | > 31 | > 32 | ]; 33 | -------------------------------------------------------------------------------- /src/06-identity-functions/27-const-annotations.solution.2.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "../helpers/type-utils"; 2 | import { F } from "ts-toolbelt"; 3 | 4 | export const asConst = (t: F.Narrow) => t; 5 | 6 | const fruits = asConst([ 7 | { 8 | name: "apple", 9 | price: 1, 10 | }, 11 | { 12 | name: "banana", 13 | price: 2, 14 | }, 15 | ]); 16 | 17 | type tests = [ 18 | Expect< 19 | Equal< 20 | typeof fruits, 21 | [ 22 | { 23 | name: "apple"; 24 | price: 1; 25 | }, 26 | { 27 | name: "banana"; 28 | price: 2; 29 | } 30 | ] 31 | > 32 | > 33 | ]; 34 | -------------------------------------------------------------------------------- /src/06-identity-functions/28-constraints-with-const-annotations.problem.ts: -------------------------------------------------------------------------------- 1 | import { it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | export const narrowFruits = (t: TFruits) => t; 5 | 6 | const fruits = narrowFruits([ 7 | { 8 | name: "apple", 9 | price: 1, 10 | }, 11 | { 12 | name: "banana", 13 | price: 2, 14 | }, 15 | ]); 16 | 17 | type tests = [ 18 | Expect< 19 | Equal< 20 | typeof fruits, 21 | readonly [ 22 | { 23 | readonly name: "apple"; 24 | readonly price: 1; 25 | }, 26 | { 27 | readonly name: "banana"; 28 | readonly price: 2; 29 | } 30 | ] 31 | > 32 | > 33 | ]; 34 | 35 | it("Should ONLY let you pass an array of fruits", () => { 36 | const notAllowed = narrowFruits([ 37 | // @ts-expect-error 38 | "not allowed", 39 | ]); 40 | }); 41 | -------------------------------------------------------------------------------- /src/06-identity-functions/28-constraints-with-const-annotations.solution.1.ts: -------------------------------------------------------------------------------- 1 | import { it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | type FruitsConstraint = readonly { 5 | name: string; 6 | price: number; 7 | }[]; 8 | 9 | export const narrowFruits = ( 10 | t: TFruits 11 | ) => t; 12 | 13 | const fruits = narrowFruits([ 14 | { 15 | name: "apple", 16 | price: 1, 17 | }, 18 | { 19 | name: "banana", 20 | price: 2, 21 | }, 22 | ]); 23 | 24 | type tests = [ 25 | Expect< 26 | Equal< 27 | typeof fruits, 28 | readonly [ 29 | { 30 | readonly name: "apple"; 31 | readonly price: 1; 32 | }, 33 | { 34 | readonly name: "banana"; 35 | readonly price: 2; 36 | } 37 | ] 38 | > 39 | > 40 | ]; 41 | 42 | it("Should ONLY let you pass an array of fruits", () => { 43 | const notAllowed = narrowFruits([ 44 | // @ts-expect-error 45 | "not allowed", 46 | ]); 47 | }); 48 | -------------------------------------------------------------------------------- /src/06-identity-functions/28-constraints-with-const-annotations.solution.2.ts: -------------------------------------------------------------------------------- 1 | import { it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | export const narrowFruits = < 5 | const TFruits extends ReadonlyArray<{ 6 | name: string; 7 | price: number; 8 | }> 9 | >( 10 | t: TFruits 11 | ) => t; 12 | 13 | const fruits = narrowFruits([ 14 | { 15 | name: "apple", 16 | price: 1, 17 | }, 18 | { 19 | name: "banana", 20 | price: 2, 21 | }, 22 | ]); 23 | 24 | type tests = [ 25 | Expect< 26 | Equal< 27 | typeof fruits, 28 | readonly [ 29 | { 30 | readonly name: "apple"; 31 | readonly price: 1; 32 | }, 33 | { 34 | readonly name: "banana"; 35 | readonly price: 2; 36 | } 37 | ] 38 | > 39 | > 40 | ]; 41 | 42 | it("Should ONLY let you pass an array of fruits", () => { 43 | const notAllowed = narrowFruits([ 44 | // @ts-expect-error 45 | "not allowed", 46 | ]); 47 | }); 48 | -------------------------------------------------------------------------------- /src/06-identity-functions/29-finite-state-machine.problem.ts: -------------------------------------------------------------------------------- 1 | type NoInfer = [T][T extends any ? 0 : never]; 2 | 3 | /** 4 | * Clue: NoInfer is part of the solution! 5 | * 6 | * You'll need to modify the interface below 7 | * to get it to work. 8 | */ 9 | interface FSMConfig { 10 | initial: TState; 11 | states: Record< 12 | TState, 13 | { 14 | onEntry?: () => void; 15 | } 16 | >; 17 | } 18 | 19 | export const makeFiniteStateMachine = ( 20 | config: FSMConfig, 21 | ) => config; 22 | 23 | const config = makeFiniteStateMachine({ 24 | initial: "a", 25 | states: { 26 | a: { 27 | onEntry: () => { 28 | console.log("a"); 29 | }, 30 | }, 31 | // b should be allowed to be specified! 32 | b: {}, 33 | }, 34 | }); 35 | 36 | const config2 = makeFiniteStateMachine({ 37 | // c should not be allowed! It doesn't exist on the states below 38 | // @ts-expect-error 39 | initial: "c", 40 | states: { 41 | a: {}, 42 | // b should be allowed to be specified! 43 | b: {}, 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /src/06-identity-functions/29-finite-state-machine.solution.ts: -------------------------------------------------------------------------------- 1 | // Before 5.4 2 | type NoInfer = [T][T extends any ? 0 : never]; 3 | 4 | interface FSMConfig { 5 | initial: NoInfer; 6 | states: Record< 7 | TState, 8 | { 9 | onEntry?: () => void; 10 | } 11 | >; 12 | } 13 | 14 | export const makeFiniteStateMachine = ( 15 | config: FSMConfig, 16 | ) => config; 17 | 18 | const config = makeFiniteStateMachine({ 19 | initial: "a", 20 | states: { 21 | a: { 22 | onEntry: () => { 23 | console.log("a"); 24 | }, 25 | }, 26 | // b should be allowed to be specified! 27 | b: {}, 28 | }, 29 | }); 30 | 31 | const config2 = makeFiniteStateMachine({ 32 | // c should not be allowed! It doesn't exist on the states below 33 | // @ts-expect-error 34 | initial: "c", 35 | states: { 36 | a: {}, 37 | // b should be allowed to be specified! 38 | b: {}, 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /src/06-identity-functions/30-no-generics-on-objects.problem.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * fetchers is an object where you can optionally 3 | * pass keys that match the route names. 4 | * 5 | * BUT - how do we prevent the user from passing 6 | * fetchers that don't exist in the routes array? 7 | * 8 | * We'll need to change this to a function which takes 9 | * in the config as an argument. 10 | * 11 | * Desired API: 12 | * 13 | * const config = makeConfigObj(config); 14 | */ 15 | 16 | export const configObj = { 17 | routes: ["/", "/about", "/contact"], 18 | fetchers: { 19 | // @ts-expect-error 20 | "/does-not-exist": () => { 21 | return {}; 22 | }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/06-identity-functions/30-no-generics-on-objects.solution.1.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This solution works, but means we need to specify the routes 3 | * TWICE - once in the routes array, and once in the generic 4 | * we pass to ConfigObj 5 | */ 6 | 7 | interface ConfigObj { 8 | routes: TRoute[]; 9 | fetchers: { 10 | [K in TRoute]?: () => any; 11 | }; 12 | } 13 | 14 | export const configObj: ConfigObj<"/" | "/about" | "/contact"> = { 15 | routes: ["/", "/about", "/contact"], 16 | fetchers: { 17 | // @ts-expect-error 18 | "/does-not-exist": () => { 19 | return {}; 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/06-identity-functions/30-no-generics-on-objects.solution.2.ts: -------------------------------------------------------------------------------- 1 | interface ConfigObj { 2 | routes: TRoute[]; 3 | fetchers: { 4 | [K in TRoute]?: () => any; 5 | }; 6 | } 7 | 8 | /** 9 | * The solution is to use an identity function containing 10 | * a generic, which will capture the names of the routes and 11 | * allow the user to specify the fetchers. 12 | */ 13 | const makeConfigObj = (config: ConfigObj) => 14 | config; 15 | 16 | export const configObj = makeConfigObj({ 17 | routes: ["/", "/about", "/contact"], 18 | fetchers: { 19 | // @ts-expect-error 20 | "/does-not-exist": () => { 21 | return {}; 22 | }, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /src/06-identity-functions/30.5-reverse-mapped-types.problem.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "../helpers/type-utils"; 2 | 3 | export function makeEventHandlers(obj: unknown) { 4 | return obj; 5 | } 6 | 7 | const obj = makeEventHandlers({ 8 | click: (name) => { 9 | console.log(name); 10 | 11 | type test = Expect>; 12 | }, 13 | focus: (name) => { 14 | console.log(name); 15 | 16 | type test = Expect>; 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/06-identity-functions/30.5-reverse-mapped-types.solution.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "../helpers/type-utils"; 2 | 3 | export function makeEventHandlers(obj: { 4 | [K in keyof T]: (name: K) => void; 5 | }) { 6 | return obj; 7 | } 8 | 9 | const obj = makeEventHandlers({ 10 | click: (name) => { 11 | console.log(name); 12 | 13 | type test = Expect>; 14 | }, 15 | focus: (name) => { 16 | console.log(name); 17 | 18 | type test = Expect>; 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/07-challenges/31-merge-dynamic-object-with-global.problem.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "../helpers/type-utils"; 2 | 3 | const addAllOfThisToWindow = { 4 | add: (a: number, b: number) => a + b, 5 | subtract: (a: number, b: number) => a - b, 6 | multiply: (a: number, b: number) => a * b, 7 | divide: (a: number, b: number) => a / b, 8 | }; 9 | 10 | Object.assign(window, addAllOfThisToWindow); 11 | 12 | type tests = [ 13 | Expect number>>, 14 | Expect number>>, 15 | Expect number>>, 16 | Expect number>>, 17 | ]; 18 | -------------------------------------------------------------------------------- /src/07-challenges/31-merge-dynamic-object-with-global.solution.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "../helpers/type-utils"; 2 | 3 | const addAllOfThisToWindow = { 4 | addSolution: (a: number, b: number) => a + b, 5 | subtractSolution: (a: number, b: number) => a - b, 6 | multiplySolution: (a: number, b: number) => a * b, 7 | divideSolution: (a: number, b: number) => a / b, 8 | }; 9 | 10 | Object.assign(window, addAllOfThisToWindow); 11 | 12 | declare global { 13 | type StuffToAdd = typeof addAllOfThisToWindow; 14 | 15 | interface Window extends StuffToAdd {} 16 | } 17 | 18 | type tests = [ 19 | Expect number>>, 20 | Expect< 21 | Equal number> 22 | >, 23 | Expect< 24 | Equal number> 25 | >, 26 | Expect number>> 27 | ]; 28 | -------------------------------------------------------------------------------- /src/07-challenges/32-narrow-with-arrays.problem.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "../helpers/type-utils"; 2 | 3 | interface Fruit { 4 | name: string; 5 | price: number; 6 | } 7 | 8 | export const wrapFruit = (fruits: unknown[]) => { 9 | const getFruit = (name: unknown) => { 10 | return fruits.find((fruit) => fruit.name === name); 11 | }; 12 | 13 | return { 14 | getFruit, 15 | }; 16 | }; 17 | 18 | const fruits = wrapFruit([ 19 | { 20 | name: "apple", 21 | price: 1, 22 | }, 23 | { 24 | name: "banana", 25 | price: 2, 26 | }, 27 | ]); 28 | 29 | const banana = fruits.getFruit("banana"); 30 | const apple = fruits.getFruit("apple"); 31 | // @ts-expect-error 32 | const notAllowed = fruits.getFruit("not-allowed"); 33 | 34 | type tests = [ 35 | Expect>, 36 | Expect> 37 | ]; 38 | -------------------------------------------------------------------------------- /src/07-challenges/32-narrow-with-arrays.solution.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "../helpers/type-utils"; 2 | 3 | interface Fruit { 4 | name: string; 5 | price: number; 6 | } 7 | 8 | export const wrapFruit = ( 9 | fruits: TFruits, 10 | ) => { 11 | const getFruit = (name: TName) => { 12 | return fruits.find((fruit) => fruit.name === name) as Extract< 13 | TFruits[number], 14 | { name: TName } 15 | >; 16 | }; 17 | 18 | return { 19 | getFruit, 20 | }; 21 | }; 22 | 23 | const fruits = wrapFruit([ 24 | { 25 | name: "apple", 26 | price: 1, 27 | }, 28 | { 29 | name: "banana", 30 | price: 2, 31 | }, 32 | ]); 33 | 34 | const banana = fruits.getFruit("banana"); 35 | const apple = fruits.getFruit("apple"); 36 | // @ts-expect-error 37 | const notAllowed = fruits.getFruit("not-allowed"); 38 | 39 | type tests = [ 40 | Expect>, 41 | Expect>, 42 | ]; 43 | -------------------------------------------------------------------------------- /src/07-challenges/33-zod-with-express.problem.ts: -------------------------------------------------------------------------------- 1 | import express, { RequestHandler } from "express"; 2 | import { it } from "vitest"; 3 | import { z, ZodError } from "zod"; 4 | import { Equal, Expect } from "../helpers/type-utils"; 5 | 6 | const makeTypeSafeHandler = ( 7 | config: { 8 | query?: z.Schema; 9 | body?: z.Schema; 10 | }, 11 | handler: RequestHandler 12 | ): RequestHandler => { 13 | return (req, res, next) => { 14 | const { query, body } = req; 15 | if (config.query) { 16 | try { 17 | config.query.parse(query); 18 | } catch (e) { 19 | return res.status(400).send((e as ZodError).message); 20 | } 21 | } 22 | if (config.body) { 23 | try { 24 | config.body.parse(body); 25 | } catch (e) { 26 | return res.status(400).send((e as ZodError).message); 27 | } 28 | } 29 | return handler(req, res, next); 30 | }; 31 | }; 32 | 33 | const app = express(); 34 | 35 | it("Should make the query AND body type safe", () => { 36 | app.get( 37 | "/users", 38 | makeTypeSafeHandler( 39 | { 40 | query: z.object({ 41 | id: z.string(), 42 | }), 43 | body: z.object({ 44 | name: z.string(), 45 | }), 46 | }, 47 | (req, res) => { 48 | type tests = [ 49 | Expect>, 50 | Expect> 51 | ]; 52 | } 53 | ) 54 | ); 55 | }); 56 | 57 | it("Should default them to any if not passed in config", () => { 58 | app.get( 59 | "/users", 60 | makeTypeSafeHandler({}, (req, res) => { 61 | type tests = [ 62 | Expect>, 63 | Expect> 64 | ]; 65 | }) 66 | ); 67 | }); 68 | -------------------------------------------------------------------------------- /src/07-challenges/33-zod-with-express.solution.ts: -------------------------------------------------------------------------------- 1 | import express, { RequestHandler } from "express"; 2 | import { it } from "vitest"; 3 | import { z, ZodError } from "zod"; 4 | import { Equal, Expect } from "../helpers/type-utils"; 5 | import { ParsedQs } from "qs"; 6 | 7 | const makeTypeSafeHandler = < 8 | TQuery extends ParsedQs = any, 9 | TBody extends Record = any 10 | >( 11 | config: { 12 | query?: z.Schema; 13 | body?: z.Schema; 14 | }, 15 | handler: RequestHandler 16 | ): RequestHandler => { 17 | return (req, res, next) => { 18 | const { query, body } = req; 19 | if (config.query) { 20 | try { 21 | config.query.parse(query); 22 | } catch (e) { 23 | return res.status(400).send((e as ZodError).message); 24 | } 25 | } 26 | if (config.body) { 27 | try { 28 | config.body.parse(body); 29 | } catch (e) { 30 | return res.status(400).send((e as ZodError).message); 31 | } 32 | } 33 | return handler(req, res, next); 34 | }; 35 | }; 36 | 37 | const app = express(); 38 | 39 | it("Should make the query AND body type safe", () => { 40 | app.get( 41 | "/users", 42 | makeTypeSafeHandler( 43 | { 44 | query: z.object({ 45 | id: z.string(), 46 | }), 47 | body: z.object({ 48 | name: z.string(), 49 | }), 50 | }, 51 | (req, res) => { 52 | type tests = [ 53 | Expect>, 54 | Expect> 55 | ]; 56 | } 57 | ) 58 | ); 59 | }); 60 | 61 | it("Should default them to any if not passed in config", () => { 62 | app.get( 63 | "/users", 64 | makeTypeSafeHandler({}, (req, res) => { 65 | type tests = [ 66 | Expect>, 67 | Expect> 68 | ]; 69 | }) 70 | ); 71 | }); 72 | -------------------------------------------------------------------------------- /src/07-challenges/34-dynamic-reducer.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | // Clue - this will be needed! 5 | type PayloadsToDiscriminatedUnion> = { 6 | [K in keyof T]: { type: K } & T[K]; 7 | }[keyof T]; 8 | 9 | /** 10 | * It turns a record of handler information into a discriminated union: 11 | * 12 | * | { type: "LOG_IN", username: string, password: string } 13 | * | { type: "LOG_OUT" } 14 | */ 15 | type TestingPayloadsToDiscriminatedUnion = PayloadsToDiscriminatedUnion<{ 16 | LOG_IN: { username: string; password: string }; 17 | LOG_OUT: {}; 18 | }>; 19 | 20 | /** 21 | * Clue: 22 | * 23 | * You'll need to add two generics here! 24 | */ 25 | export class DynamicReducer { 26 | private handlers = {} as unknown; 27 | 28 | addHandler( 29 | type: unknown, 30 | handler: (state: unknown, payload: unknown) => unknown 31 | ): unknown { 32 | this.handlers[type] = handler; 33 | 34 | return this; 35 | } 36 | 37 | reduce(state: unknown, action: unknown): unknown { 38 | const handler = this.handlers[action.type]; 39 | if (!handler) { 40 | return state; 41 | } 42 | 43 | return handler(state, action); 44 | } 45 | } 46 | 47 | interface State { 48 | username: string; 49 | password: string; 50 | } 51 | 52 | const reducer = new DynamicReducer() 53 | .addHandler( 54 | "LOG_IN", 55 | (state, action: { username: string; password: string }) => { 56 | return { 57 | username: action.username, 58 | password: action.password, 59 | }; 60 | } 61 | ) 62 | .addHandler("LOG_OUT", () => { 63 | return { 64 | username: "", 65 | password: "", 66 | }; 67 | }); 68 | 69 | it("Should return the new state after LOG_IN", () => { 70 | const state = reducer.reduce( 71 | { username: "", password: "" }, 72 | { type: "LOG_IN", username: "foo", password: "bar" } 73 | ); 74 | 75 | type test = [Expect>]; 76 | 77 | expect(state).toEqual({ username: "foo", password: "bar" }); 78 | }); 79 | 80 | it("Should return the new state after LOG_OUT", () => { 81 | const state = reducer.reduce( 82 | { username: "foo", password: "bar" }, 83 | { type: "LOG_OUT" } 84 | ); 85 | 86 | type test = [Expect>]; 87 | 88 | expect(state).toEqual({ username: "", password: "" }); 89 | }); 90 | 91 | it("Should error if you pass it an incorrect action", () => { 92 | const state = reducer.reduce( 93 | { username: "foo", password: "bar" }, 94 | { 95 | // @ts-expect-error 96 | type: "NOT_ALLOWED", 97 | } 98 | ); 99 | }); 100 | 101 | it("Should error if you pass an incorrect payload", () => { 102 | const state = reducer.reduce( 103 | { username: "foo", password: "bar" }, 104 | // @ts-expect-error 105 | { 106 | type: "LOG_IN", 107 | } 108 | ); 109 | }); 110 | -------------------------------------------------------------------------------- /src/07-challenges/34-dynamic-reducer.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "../helpers/type-utils"; 3 | 4 | type PayloadsToDiscriminatedUnion> = { 5 | [K in keyof T]: { type: K } & T[K]; 6 | }[keyof T]; 7 | 8 | export class DynamicReducer< 9 | TState, 10 | TPayloadMap extends Record = {}, 11 | > { 12 | private handlers = {} as Record< 13 | string, 14 | (state: TState, payload: any) => TState 15 | >; 16 | 17 | addHandler( 18 | type: TType, 19 | handler: (state: TState, payload: TPayload) => TState, 20 | ): DynamicReducer> { 21 | this.handlers[type] = handler; 22 | 23 | return this; 24 | } 25 | 26 | reduce( 27 | state: TState, 28 | action: PayloadsToDiscriminatedUnion, 29 | ): TState { 30 | const handler = this.handlers[action.type]; 31 | if (!handler) { 32 | return state; 33 | } 34 | 35 | return handler(state, action); 36 | } 37 | } 38 | 39 | interface State { 40 | username: string; 41 | password: string; 42 | } 43 | 44 | const reducer = new DynamicReducer() 45 | .addHandler( 46 | "LOG_IN", 47 | (state, action: { username: string; password: string }) => { 48 | return { 49 | username: action.username, 50 | password: action.password, 51 | }; 52 | }, 53 | ) 54 | .addHandler("LOG_OUT", () => { 55 | return { 56 | username: "", 57 | password: "", 58 | }; 59 | }) 60 | .addHandler("UPDATE_USERNAME", (state, action: { username: string }) => { 61 | return { 62 | ...state, 63 | username: action.username, 64 | }; 65 | }); 66 | 67 | it("Should return the new state after LOG_IN", () => { 68 | const state = reducer.reduce( 69 | { username: "", password: "" }, 70 | { 71 | type: "UPDATE_USERNAME", 72 | username: "new-password", 73 | }, 74 | ); 75 | 76 | type test = [Expect>]; 77 | 78 | expect(state).toEqual({ username: "new-password", password: "" }); 79 | }); 80 | 81 | it("Should return the new state after LOG_OUT", () => { 82 | const state = reducer.reduce( 83 | { username: "foo", password: "bar" }, 84 | { type: "LOG_OUT" }, 85 | ); 86 | 87 | type test = [Expect>]; 88 | 89 | expect(state).toEqual({ username: "", password: "" }); 90 | }); 91 | 92 | it("Should error if you pass it an incorrect action", () => { 93 | const state = reducer.reduce( 94 | { username: "foo", password: "bar" }, 95 | { 96 | // @ts-expect-error 97 | type: "NOT_ALLOWED", 98 | }, 99 | ); 100 | }); 101 | 102 | it("Should error if you pass an incorrect payload", () => { 103 | const state = reducer.reduce( 104 | { username: "foo", password: "bar" }, 105 | // @ts-expect-error 106 | { 107 | type: "LOG_IN", 108 | }, 109 | ); 110 | }); 111 | -------------------------------------------------------------------------------- /src/07-challenges/38-challenge-custom-jsx-element.problem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | /** 4 | * How do we add a new base element to React's JSX? 5 | * 6 | * You'll need to do some detective work: check 7 | * out JSX.IntrinsicElements. 8 | * 9 | * The JSX namespace comes from React - you'll need 10 | * to check out React's type definitions. 11 | */ 12 | 13 | const element = hello world; 14 | -------------------------------------------------------------------------------- /src/07-challenges/38-challenge-custom-jsx-element.solution.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | /** 4 | * As a bonus, how do we make sure that it has 5 | * some required props? 6 | */ 7 | 8 | declare global { 9 | namespace JSX { 10 | interface IntrinsicElements { 11 | "custom-solution-element": { 12 | children?: React.ReactNode; 13 | }; 14 | } 15 | } 16 | } 17 | 18 | const element = hello world; 19 | -------------------------------------------------------------------------------- /src/fake-animation-lib/index.ts: -------------------------------------------------------------------------------- 1 | export const getAnimatingState = (): string => { 2 | if (Math.random() > 0.5) { 3 | return "before-animation"; 4 | } 5 | 6 | if (Math.random() > 0.5) { 7 | return "animating"; 8 | } 9 | 10 | return "after-animation"; 11 | }; 12 | -------------------------------------------------------------------------------- /src/fake-external-lib/fetches.ts: -------------------------------------------------------------------------------- 1 | export const fetchUser = async (id: string) => { 2 | return { 3 | id, 4 | firstName: "John", 5 | lastName: "Doe", 6 | }; 7 | }; 8 | 9 | export const fetchPost = async (id: string) => { 10 | return { 11 | id, 12 | title: "Hello World", 13 | body: "This is a post that is great and is excessively long, much too long for an excerpt.", 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/fake-external-lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./typePredicates"; 2 | export * from "./fetches"; 3 | export * from "./nonNullable"; 4 | -------------------------------------------------------------------------------- /src/fake-external-lib/nonNullable.ts: -------------------------------------------------------------------------------- 1 | export const isNonNullable = (value: T): value is NonNullable => 2 | value !== null && value !== undefined; 3 | -------------------------------------------------------------------------------- /src/fake-external-lib/typePredicates.ts: -------------------------------------------------------------------------------- 1 | export const isDivElement = (element: unknown): element is HTMLDivElement => { 2 | return element instanceof HTMLDivElement; 3 | }; 4 | 5 | export const isBodyElement = (element: unknown): element is HTMLBodyElement => { 6 | return element instanceof HTMLBodyElement; 7 | }; 8 | -------------------------------------------------------------------------------- /src/helpers/Brand.ts: -------------------------------------------------------------------------------- 1 | declare const brand: unique symbol; 2 | 3 | export type Brand = T & { [brand]: TBrand }; 4 | -------------------------------------------------------------------------------- /src/helpers/type-utils.ts: -------------------------------------------------------------------------------- 1 | export type Expect = T; 2 | export type ExpectTrue = T; 3 | export type ExpectFalse = T; 4 | export type IsTrue = T; 5 | export type IsFalse = T; 6 | 7 | export type Equal = (() => T extends X ? 1 : 2) extends < 8 | T, 9 | >() => T extends Y ? 1 : 2 10 | ? true 11 | : false; 12 | export type NotEqual = true extends Equal ? false : true; 13 | 14 | // https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360 15 | export type IsAny = 0 extends 1 & T ? true : false; 16 | export type NotAny = true extends IsAny ? false : true; 17 | 18 | export type Debug = { [K in keyof T]: T[K] }; 19 | export type MergeInsertions = T extends object 20 | ? { [K in keyof T]: MergeInsertions } 21 | : T; 22 | 23 | export type Alike = Equal, MergeInsertions>; 24 | 25 | export type ExpectExtends = EXPECTED extends VALUE 26 | ? true 27 | : false; 28 | export type ExpectValidArgs< 29 | FUNC extends (...args: any[]) => any, 30 | ARGS extends any[], 31 | > = ARGS extends Parameters ? true : false; 32 | 33 | export type UnionToIntersection = ( 34 | U extends any ? (k: U) => void : never 35 | ) extends (k: infer I) => void 36 | ? I 37 | : never; 38 | 39 | export const doNotExecute = (func: () => void) => {}; 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [ 4 | "vitest/importMeta" 5 | ], 6 | "target": "es2020", 7 | "module": "ES2022", 8 | "moduleResolution": "node", 9 | "noEmit": true, 10 | "jsx": "preserve", 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "strict": true, 15 | "skipLibCheck": true, 16 | "paths": { 17 | "fake-external-lib": [ 18 | "./src/fake-external-lib/index" 19 | ], 20 | "fake-animation-lib": [ 21 | "./src/fake-animation-lib/index" 22 | ], 23 | "fake-animation-lib-solution": [ 24 | "./src/fake-animation-lib/index" 25 | ] 26 | } 27 | }, 28 | "include": [ 29 | "src" 30 | ] 31 | } -------------------------------------------------------------------------------- /vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | import tsconfigPaths from "vite-tsconfig-paths"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | include: ["src/**/*{problem,solution,explainer}*.{ts,tsx}"], 7 | passWithNoTests: true, 8 | environment: "jsdom", 9 | }, 10 | plugins: [tsconfigPaths()], 11 | }); 12 | --------------------------------------------------------------------------------