├── .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 |
--------------------------------------------------------------------------------