├── .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-inference-basics
│ ├── 00-intro.explainer.ts
│ ├── 01-get-function-return-type.problem.ts
│ ├── 01-get-function-return-type.solution.ts
│ ├── 01.5-gabriel-typeof-keyword.explainer.ts
│ ├── 02-get-function-parameters.problem.ts
│ ├── 02-get-function-parameters.solution.ts
│ ├── 03-awaited.problem.ts
│ ├── 03-awaited.solution.ts
│ ├── 04-get-object-keys.problem.ts
│ └── 04-get-object-keys.solution.ts
├── 02-unions-and-indexing
│ ├── 05-terminology.problem.ts
│ ├── 05-terminology.solution.ts
│ ├── 06-extract-from-discriminated-union.problem.ts
│ ├── 06-extract-from-discriminated-union.solution.ts
│ ├── 07-exclude-from-discriminated-union.problem.ts
│ ├── 07-exclude-from-discriminated-union.solution.ts
│ ├── 07.5-union-type-multiverse.explainer.ts
│ ├── 08-indexed-access.problem.ts
│ ├── 08-indexed-access.solution.ts
│ ├── 09-discriminated-union-to-discriminator.problem.ts
│ ├── 09-discriminated-union-to-discriminator.solution.ts
│ ├── 10-as-const.problem.ts
│ ├── 10-as-const.solution.ts
│ ├── 11-indexed-access-with-unions.problem.ts
│ ├── 11-indexed-access-with-unions.solution.1.ts
│ ├── 11-indexed-access-with-unions.solution.2.ts
│ ├── 12-get-object-values.problem.ts
│ ├── 12-get-object-values.solution.ts
│ ├── 13-get-array-value.problem.ts
│ └── 13-get-array-value.solution.ts
├── 03-template-literals
│ ├── 14-template-literal-with-string.problem.ts
│ ├── 14-template-literal-with-string.solution.ts
│ ├── 15-extract-with-template-literals.problem.ts
│ ├── 15-extract-with-template-literals.solution.ts
│ ├── 16-unions-in-template-literals.problem.ts
│ ├── 16-unions-in-template-literals.solution.ts
│ ├── 17-splitting-strings.problem.ts
│ ├── 17-splitting-strings.solution.ts
│ ├── 18-template-literals-in-object-keys.problem.ts
│ ├── 18-template-literals-in-object-keys.solution.ts
│ ├── 19-uppercase-object.problem.ts
│ └── 19-uppercase-object.solution.ts
├── 03.5-type-helpers-pattern
│ ├── 20-type-helpers-pattern.problem.ts
│ ├── 20-type-helpers-pattern.solution.ts
│ ├── 20.1-maybe.problem.ts
│ ├── 20.1-maybe.solution.ts
│ ├── 20.2-constraints.problem.ts
│ ├── 20.2-constraints.solution.ts
│ ├── 20.3-multiple.problem.ts
│ ├── 20.3-multiple.solution.ts
│ ├── 20.4-defaults.problem.ts
│ ├── 20.4-defaults.solution.ts
│ ├── 20.5-function-constraints.problem.ts
│ ├── 20.5-function-constraints.solution.ts
│ ├── 20.6-not-undefined-or-null-constraint.problem.ts
│ ├── 20.6-not-undefined-or-null-constraint.solution.ts
│ ├── 20.7-non-empty-array.problem.ts
│ └── 20.7-non-empty-array.solution.ts
├── 04-conditional-types-and-infer
│ ├── 21-conditional-types.problem.ts
│ ├── 21-conditional-types.solution.ts
│ ├── 22-returning-never.problem.ts
│ ├── 22-returning-never.solution.ts
│ ├── 22.5-why-conditional-types-are-needed.explainer.ts
│ ├── 23-infer-with-raw-values.problem.ts
│ ├── 23-infer-with-raw-values.solution.1.ts
│ ├── 23-infer-with-raw-values.solution.2.ts
│ ├── 24-infer-with-generics.problem.ts
│ ├── 24-infer-with-generics.solution.ts
│ ├── 25-template-literal-value-extraction.problem.ts
│ ├── 25-template-literal-value-extraction.solution.1.ts
│ ├── 25-template-literal-value-extraction.solution.2.ts
│ ├── 25.5-regex-types.explainer.ts
│ ├── 26-get-result-from-async-function.problem.ts
│ ├── 26-get-result-from-async-function.solution.ts
│ ├── 27-infer-in-union-types.problem.ts
│ ├── 27-infer-in-union-types.solution.1.ts
│ ├── 27-infer-in-union-types.solution.2.ts
│ ├── 28-distributive-conditional-types.problem.ts
│ ├── 28-distributive-conditional-types.solution.1.ts
│ └── 28-distributive-conditional-types.solution.2.ts
├── 05-key-remapping
│ ├── 29-union-to-object.problem.ts
│ ├── 29-union-to-object.solution.ts
│ ├── 30-k-in-keyof.problem.ts
│ ├── 30-k-in-keyof.solution.ts
│ ├── 31-k-in-keyof-as.problem.ts
│ ├── 31-k-in-keyof-as.solution.ts
│ ├── 32-never-in-key-remapping.problem.ts
│ ├── 32-never-in-key-remapping.solution.ts
│ ├── 33-discriminated-union-to-object.problem.ts
│ ├── 33-discriminated-union-to-object.solution.1.ts
│ ├── 33-discriminated-union-to-object.solution.2.ts
│ ├── 34-object-to-union-of-tuples.problem.ts
│ ├── 34-object-to-union-of-tuples.solution.ts
│ ├── 35-object-to-union-of-template-literals.problem.ts
│ ├── 35-object-to-union-of-template-literals.solution.ts
│ ├── 36-discriminated-union-to-union.problem.ts
│ └── 36-discriminated-union-to-union.solution.ts
├── 06-challenges
│ ├── 37-get-dynamic-path-params.problem.ts
│ ├── 37-get-dynamic-path-params.solution.ts
│ ├── 38-mutually-exclusive-properties.problem.ts
│ ├── 38-mutually-exclusive-properties.solution.ts
│ ├── 39-discriminated-union-with-unique-values-to-object.problem.ts
│ ├── 39-discriminated-union-with-unique-values-to-object.solution.ts
│ ├── 40-deep-partial.problem.ts
│ └── 40-deep-partial.solution.ts
└── helpers
│ └── 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/type-transformations-tutorial).
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/type-transformations-workshop/7e8bc853fde706c9fdbb5f68168331848f454859/og-image-new.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "type-transformations",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "author": "Matt Pocock ",
6 | "license": "GPL",
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 | "ts-toolbelt": "^9.6.0",
14 | "typescript": "^5.4.5",
15 | "vite-tsconfig-paths": "^5.0.0",
16 | "vitest": "^2.0.0"
17 | },
18 | "scripts": {
19 | "exercise": "tt-cli run",
20 | "e": "npm run exercise",
21 | "solution": "tt-cli run --solution",
22 | "s": "npm run solution",
23 | "ci": "(cd scripts/tests && npx vitest run)",
24 | "prepare": "tt-cli prepare-stackblitz",
25 | "update-snapshots": "(cd scripts/tests && npx vitest run -u)",
26 | "e-01": "tt-cli run 01",
27 | "s-01": "tt-cli run 01 --solution",
28 | "e-02": "tt-cli run 02",
29 | "s-02": "tt-cli run 02 --solution",
30 | "e-03": "tt-cli run 03",
31 | "s-03": "tt-cli run 03 --solution",
32 | "e-04": "tt-cli run 04",
33 | "s-04": "tt-cli run 04 --solution",
34 | "e-05": "tt-cli run 05",
35 | "s-05": "tt-cli run 05 --solution",
36 | "e-06": "tt-cli run 06",
37 | "s-06": "tt-cli run 06 --solution",
38 | "e-07": "tt-cli run 07",
39 | "s-07": "tt-cli run 07 --solution",
40 | "e-08": "tt-cli run 08",
41 | "s-08": "tt-cli run 08 --solution",
42 | "e-09": "tt-cli run 09",
43 | "s-09": "tt-cli run 09 --solution",
44 | "e-10": "tt-cli run 10",
45 | "s-10": "tt-cli run 10 --solution",
46 | "e-11": "tt-cli run 11",
47 | "s-11": "tt-cli run 11 --solution",
48 | "e-12": "tt-cli run 12",
49 | "s-12": "tt-cli run 12 --solution",
50 | "e-13": "tt-cli run 13",
51 | "s-13": "tt-cli run 13 --solution",
52 | "e-14": "tt-cli run 14",
53 | "s-14": "tt-cli run 14 --solution",
54 | "e-15": "tt-cli run 15",
55 | "s-15": "tt-cli run 15 --solution",
56 | "e-16": "tt-cli run 16",
57 | "s-16": "tt-cli run 16 --solution",
58 | "e-17": "tt-cli run 17",
59 | "s-17": "tt-cli run 17 --solution",
60 | "e-18": "tt-cli run 18",
61 | "s-18": "tt-cli run 18 --solution",
62 | "e-19": "tt-cli run 19",
63 | "s-19": "tt-cli run 19 --solution",
64 | "e-20": "tt-cli run 20",
65 | "s-20": "tt-cli run 20 --solution",
66 | "e-21": "tt-cli run 21",
67 | "s-21": "tt-cli run 21 --solution",
68 | "e-22": "tt-cli run 22",
69 | "s-22": "tt-cli run 22 --solution",
70 | "e-23": "tt-cli run 23",
71 | "s-23": "tt-cli run 23 --solution",
72 | "e-24": "tt-cli run 24",
73 | "s-24": "tt-cli run 24 --solution",
74 | "e-25": "tt-cli run 25",
75 | "s-25": "tt-cli run 25 --solution",
76 | "e-26": "tt-cli run 26",
77 | "s-26": "tt-cli run 26 --solution",
78 | "e-27": "tt-cli run 27",
79 | "s-27": "tt-cli run 27 --solution",
80 | "e-28": "tt-cli run 28",
81 | "s-28": "tt-cli run 28 --solution",
82 | "e-29": "tt-cli run 29",
83 | "s-29": "tt-cli run 29 --solution",
84 | "e-30": "tt-cli run 30",
85 | "s-30": "tt-cli run 30 --solution",
86 | "e-31": "tt-cli run 31",
87 | "s-31": "tt-cli run 31 --solution",
88 | "e-32": "tt-cli run 32",
89 | "s-32": "tt-cli run 32 --solution",
90 | "e-33": "tt-cli run 33",
91 | "s-33": "tt-cli run 33 --solution",
92 | "e-34": "tt-cli run 34",
93 | "s-34": "tt-cli run 34 --solution",
94 | "e-35": "tt-cli run 35",
95 | "s-35": "tt-cli run 35 --solution",
96 | "e-36": "tt-cli run 36",
97 | "s-36": "tt-cli run 36 --solution",
98 | "e-37": "tt-cli run 37",
99 | "s-37": "tt-cli run 37 --solution",
100 | "e-38": "tt-cli run 38",
101 | "s-38": "tt-cli run 38 --solution",
102 | "e-39": "tt-cli run 39",
103 | "s-39": "tt-cli run 39 --solution",
104 | "e-40": "tt-cli run 40",
105 | "s-40": "tt-cli run 40 --solution",
106 | "e-20.1": "tt-cli run 20.1",
107 | "s-20.1": "tt-cli run 20.1 --solution",
108 | "e-20.2": "tt-cli run 20.2",
109 | "s-20.2": "tt-cli run 20.2 --solution",
110 | "e-20.3": "tt-cli run 20.3",
111 | "s-20.3": "tt-cli run 20.3 --solution",
112 | "e-20.4": "tt-cli run 20.4",
113 | "s-20.4": "tt-cli run 20.4 --solution",
114 | "e-20.5": "tt-cli run 20.5",
115 | "s-20.5": "tt-cli run 20.5 --solution",
116 | "e-20.6": "tt-cli run 20.6",
117 | "s-20.6": "tt-cli run 20.6 --solution",
118 | "e-20.7": "tt-cli run 20.7",
119 | "s-20.7": "tt-cli run 20.7 --solution",
120 | "e-00": "tt-cli run 00",
121 | "s-00": "tt-cli run 00 --solution",
122 | "e-01.5": "tt-cli run 01.5",
123 | "s-01.5": "tt-cli run 01.5 --solution",
124 | "e-07.5": "tt-cli run 07.5",
125 | "s-07.5": "tt-cli run 07.5 --solution",
126 | "e-22.5": "tt-cli run 22.5",
127 | "s-22.5": "tt-cli run 22.5 --solution",
128 | "e-25.5": "tt-cli run 25.5",
129 | "s-25.5": "tt-cli run 25.5 --solution"
130 | },
131 | "dependencies": {
132 | "@types/express": "^4.17.13",
133 | "express": "^4.18.1",
134 | "zod": "^3.17.10"
135 | },
136 | "type": "module"
137 | }
138 |
--------------------------------------------------------------------------------
/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-inference-basics/00-intro.explainer.ts:
--------------------------------------------------------------------------------
1 | // Matt explains the course
2 |
3 | export {};
4 |
--------------------------------------------------------------------------------
/src/01-inference-basics/01-get-function-return-type.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const myFunc = () => {
4 | return "hello";
5 | };
6 |
7 | /**
8 | * How do we extract MyFuncReturn from myFunc?
9 | */
10 | type MyFuncReturn = unknown;
11 |
12 | type tests = [Expect>];
13 |
--------------------------------------------------------------------------------
/src/01-inference-basics/01-get-function-return-type.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const myFunc = () => {
4 | return "hello";
5 | };
6 |
7 | /**
8 | * How do we extract MyFuncReturn from myFunc?
9 | */
10 | type MyFuncReturn = ReturnType;
11 |
12 | type tests = [Expect>];
13 |
--------------------------------------------------------------------------------
/src/01-inference-basics/01.5-gabriel-typeof-keyword.explainer.ts:
--------------------------------------------------------------------------------
1 | // Gabriel Vergnaud explains the typeof keyword, and the type/value level
2 |
3 | export {};
4 |
--------------------------------------------------------------------------------
/src/01-inference-basics/02-get-function-parameters.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const makeQuery = (
4 | url: string,
5 | opts?: {
6 | method?: string;
7 | headers?: {
8 | [key: string]: string;
9 | };
10 | body?: string;
11 | },
12 | ) => {};
13 |
14 | type MakeQueryParameters = unknown;
15 |
16 | type tests = [
17 | Expect<
18 | Equal<
19 | MakeQueryParameters,
20 | [
21 | url: string,
22 | opts?: {
23 | method?: string;
24 | headers?: {
25 | [key: string]: string;
26 | };
27 | body?: string;
28 | },
29 | ]
30 | >
31 | >,
32 | ];
33 |
--------------------------------------------------------------------------------
/src/01-inference-basics/02-get-function-parameters.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const makeQuery = (
4 | url: string,
5 | opts?: {
6 | method?: string;
7 | headers?: {
8 | [key: string]: string;
9 | };
10 | body?: string;
11 | },
12 | ) => {};
13 |
14 | type MakeQueryParameters = Parameters;
15 |
16 | type tests = [
17 | Expect<
18 | Equal<
19 | MakeQueryParameters,
20 | [
21 | url: string,
22 | opts?: {
23 | method?: string;
24 | headers?: {
25 | [key: string]: string;
26 | };
27 | body?: string;
28 | },
29 | ]
30 | >
31 | >,
32 | ];
33 |
--------------------------------------------------------------------------------
/src/01-inference-basics/03-awaited.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const getUser = () => {
4 | return Promise.resolve({
5 | id: "123",
6 | name: "John",
7 | email: "john@example.com",
8 | });
9 | };
10 |
11 | type ReturnValue = ReturnType;
12 |
13 | type tests = [
14 | Expect>,
15 | ];
16 |
--------------------------------------------------------------------------------
/src/01-inference-basics/03-awaited.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const getUser = () => {
4 | return Promise.resolve({
5 | id: "123",
6 | name: "John",
7 | email: "john@example.com",
8 | });
9 | };
10 |
11 | type ReturnValue = Awaited>;
12 |
13 | type tests = [
14 | Expect>,
15 | ];
16 |
--------------------------------------------------------------------------------
/src/01-inference-basics/04-get-object-keys.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const testingFrameworks = {
4 | vitest: {
5 | label: "Vitest",
6 | },
7 | jest: {
8 | label: "Jest",
9 | },
10 | mocha: {
11 | label: "Mocha",
12 | },
13 | };
14 |
15 | type TestingFramework = unknown;
16 |
17 | type tests = [Expect>];
18 |
--------------------------------------------------------------------------------
/src/01-inference-basics/04-get-object-keys.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const testingFrameworks = {
4 | vitest: {
5 | label: "Vitest",
6 | },
7 | jest: {
8 | label: "Jest",
9 | },
10 | mocha: {
11 | label: "Mocha",
12 | },
13 | };
14 |
15 | type TestingFramework = keyof typeof testingFrameworks;
16 |
17 | type tests = [Expect>];
18 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/05-terminology.problem.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * It's important to understand the terminology around unions:
3 | *
4 | * One of the type declarations below is a union.
5 | * One of the type declarations below is a discriminated union.
6 | * One of the type declarations below is an enum.
7 | *
8 | * Which is which?
9 | */
10 |
11 | type A =
12 | | {
13 | type: "a";
14 | a: string;
15 | }
16 | | {
17 | type: "b";
18 | b: string;
19 | }
20 | | {
21 | type: "c";
22 | c: string;
23 | };
24 |
25 | type B = "a" | "b" | "c";
26 |
27 | enum C {
28 | A = "a",
29 | B = "b",
30 | C = "c",
31 | }
32 |
33 | export {};
34 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/05-terminology.solution.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A is a discriminated union, with 'type' as the discriminator.
3 | *
4 | * Discriminated unions have common properties which are used to
5 | * differentiate between members of the union. In this case, type
6 | * is the discriminator.
7 | */
8 | type A =
9 | | {
10 | type: "a";
11 | a: string;
12 | }
13 | | {
14 | type: "b";
15 | b: string;
16 | }
17 | | {
18 | type: "c";
19 | c: string;
20 | };
21 |
22 | /**
23 | * B is a union, but not a discriminated union.
24 | */
25 | type B = "a" | "b" | "c";
26 |
27 | /**
28 | * C is an enum.
29 | */
30 | enum C {
31 | A = "a",
32 | B = "b",
33 | C = "c",
34 | }
35 |
36 | export {};
37 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/06-extract-from-discriminated-union.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | export type Event =
4 | | {
5 | type: "click";
6 | event: MouseEvent;
7 | }
8 | | {
9 | type: "focus";
10 | event: FocusEvent;
11 | }
12 | | {
13 | type: "keydown";
14 | event: KeyboardEvent;
15 | };
16 |
17 | type ClickEvent = unknown;
18 |
19 | type tests = [Expect>];
20 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/06-extract-from-discriminated-union.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | export type Event =
4 | | {
5 | type: "click";
6 | event: MouseEvent;
7 | }
8 | | {
9 | type: "focus";
10 | event: FocusEvent;
11 | }
12 | | {
13 | type: "keydown";
14 | event: KeyboardEvent;
15 | };
16 |
17 | type ClickEvent = Extract;
18 |
19 | type tests = [Expect>];
20 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/07-exclude-from-discriminated-union.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | export type Event =
4 | | {
5 | type: "click";
6 | event: MouseEvent;
7 | }
8 | | {
9 | type: "focus";
10 | event: FocusEvent;
11 | }
12 | | {
13 | type: "keydown";
14 | event: KeyboardEvent;
15 | };
16 |
17 | type NonKeyDownEvents = unknown;
18 |
19 | type tests = [
20 | Expect<
21 | Equal<
22 | NonKeyDownEvents,
23 | | { type: "click"; event: MouseEvent }
24 | | { type: "focus"; event: FocusEvent }
25 | >
26 | >,
27 | ];
28 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/07-exclude-from-discriminated-union.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | export type Event =
4 | | {
5 | type: "click";
6 | event: MouseEvent;
7 | }
8 | | {
9 | type: "focus";
10 | event: FocusEvent;
11 | }
12 | | {
13 | type: "keydown";
14 | event: KeyboardEvent;
15 | };
16 |
17 | type NonKeyDownEvents = Exclude;
18 |
19 | type tests = [
20 | Expect<
21 | Equal<
22 | NonKeyDownEvents,
23 | | { type: "click"; event: MouseEvent }
24 | | { type: "focus"; event: FocusEvent }
25 | >
26 | >,
27 | ];
28 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/07.5-union-type-multiverse.explainer.ts:
--------------------------------------------------------------------------------
1 | // Gabriel Vergnaud explains his idea of the union type multiverse
2 |
3 | export {};
4 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/08-indexed-access.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | export const fakeDataDefaults = {
4 | String: "Default string",
5 | Int: 1,
6 | Float: 1.14,
7 | Boolean: true,
8 | ID: "id",
9 | };
10 |
11 | export type StringType = unknown;
12 | export type IntType = unknown;
13 | export type FloatType = unknown;
14 | export type BooleanType = unknown;
15 | export type IDType = unknown;
16 |
17 | type tests = [
18 | Expect>,
19 | Expect>,
20 | Expect>,
21 | Expect>,
22 | Expect>,
23 | ];
24 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/08-indexed-access.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | export const fakeDataDefaults = {
4 | String: "Default string",
5 | Int: 1,
6 | Float: 1.14,
7 | Boolean: true,
8 | ID: "id",
9 | };
10 |
11 | type FakeDataDefaults = typeof fakeDataDefaults;
12 |
13 | export type StringType = FakeDataDefaults["String"];
14 | export type IntType = FakeDataDefaults["Int"];
15 | export type FloatType = FakeDataDefaults["Float"];
16 | export type BooleanType = FakeDataDefaults["Boolean"];
17 | export type IDType = FakeDataDefaults["ID"];
18 |
19 | type tests = [
20 | Expect>,
21 | Expect>,
22 | Expect>,
23 | Expect>,
24 | Expect>
25 | ];
26 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/09-discriminated-union-to-discriminator.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | export type Event =
4 | | {
5 | type: "click";
6 | event: MouseEvent;
7 | }
8 | | {
9 | type: "focus";
10 | event: FocusEvent;
11 | }
12 | | {
13 | type: "keydown";
14 | event: KeyboardEvent;
15 | };
16 |
17 | type EventType = unknown;
18 |
19 | type tests = [Expect>];
20 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/09-discriminated-union-to-discriminator.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | export type Event =
4 | | {
5 | type: "click";
6 | event: MouseEvent;
7 | }
8 | | {
9 | type: "focus";
10 | event: FocusEvent;
11 | }
12 | | {
13 | type: "keydown";
14 | event: KeyboardEvent;
15 | };
16 |
17 | type EventType = Event["type"];
18 |
19 | type tests = [Expect>];
20 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/10-as-const.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | /**
4 | * Some docs that might help!
5 | * https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions
6 | */
7 | export const programModeEnumMap = {
8 | GROUP: "group",
9 | ANNOUNCEMENT: "announcement",
10 | ONE_ON_ONE: "1on1",
11 | SELF_DIRECTED: "selfDirected",
12 | PLANNED_ONE_ON_ONE: "planned1on1",
13 | PLANNED_SELF_DIRECTED: "plannedSelfDirected",
14 | };
15 |
16 | export type GroupProgram = typeof programModeEnumMap["GROUP"];
17 | export type AnnouncementProgram = typeof programModeEnumMap["ANNOUNCEMENT"];
18 | export type OneOnOneProgram = typeof programModeEnumMap["ONE_ON_ONE"];
19 | export type SelfDirectedProgram = typeof programModeEnumMap["SELF_DIRECTED"];
20 | export type PlannedOneOnOneProgram =
21 | typeof programModeEnumMap["PLANNED_ONE_ON_ONE"];
22 | export type PlannedSelfDirectedProgram =
23 | typeof programModeEnumMap["PLANNED_SELF_DIRECTED"];
24 |
25 | type tests = [
26 | Expect>,
27 | Expect>,
28 | Expect>,
29 | Expect>,
30 | Expect>,
31 | Expect>,
32 | ];
33 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/10-as-const.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | /**
4 | * Some docs that might help!
5 | * https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions
6 | */
7 | export const programModeEnumMap = {
8 | GROUP: "group",
9 | ANNOUNCEMENT: "announcement",
10 | ONE_ON_ONE: "1on1",
11 | SELF_DIRECTED: "selfDirected",
12 | PLANNED_ONE_ON_ONE: "planned1on1",
13 | PLANNED_SELF_DIRECTED: "plannedSelfDirected",
14 | } as const;
15 |
16 | export type GroupProgram = typeof programModeEnumMap["GROUP"];
17 | export type AnnouncementProgram = typeof programModeEnumMap["ANNOUNCEMENT"];
18 | export type OneOnOneProgram = typeof programModeEnumMap["ONE_ON_ONE"];
19 | export type SelfDirectedProgram = typeof programModeEnumMap["SELF_DIRECTED"];
20 | export type PlannedOneOnOneProgram =
21 | typeof programModeEnumMap["PLANNED_ONE_ON_ONE"];
22 | export type PlannedSelfDirectedProgram =
23 | typeof programModeEnumMap["PLANNED_SELF_DIRECTED"];
24 |
25 | type tests = [
26 | Expect>,
27 | Expect>,
28 | Expect>,
29 | Expect>,
30 | Expect>,
31 | Expect>,
32 | ];
33 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/11-indexed-access-with-unions.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | export const programModeEnumMap = {
4 | GROUP: "group",
5 | ANNOUNCEMENT: "announcement",
6 | ONE_ON_ONE: "1on1",
7 | SELF_DIRECTED: "selfDirected",
8 | PLANNED_ONE_ON_ONE: "planned1on1",
9 | PLANNED_SELF_DIRECTED: "plannedSelfDirected",
10 | } as const;
11 |
12 | export type IndividualProgram = unknown;
13 |
14 | type tests = [
15 | Expect<
16 | Equal<
17 | IndividualProgram,
18 | "1on1" | "selfDirected" | "planned1on1" | "plannedSelfDirected"
19 | >
20 | >,
21 | ];
22 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/11-indexed-access-with-unions.solution.1.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | export const programModeEnumMap = {
4 | GROUP: "group",
5 | ANNOUNCEMENT: "announcement",
6 | ONE_ON_ONE: "1on1",
7 | SELF_DIRECTED: "selfDirected",
8 | PLANNED_ONE_ON_ONE: "planned1on1",
9 | PLANNED_SELF_DIRECTED: "plannedSelfDirected",
10 | } as const;
11 |
12 | export type IndividualProgram = typeof programModeEnumMap[
13 | | "ONE_ON_ONE"
14 | | "SELF_DIRECTED"
15 | | "PLANNED_ONE_ON_ONE"
16 | | "PLANNED_SELF_DIRECTED"];
17 |
18 | type tests = [
19 | Expect<
20 | Equal<
21 | IndividualProgram,
22 | "1on1" | "selfDirected" | "planned1on1" | "plannedSelfDirected"
23 | >
24 | >,
25 | ];
26 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/11-indexed-access-with-unions.solution.2.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | export const programModeEnumMap = {
4 | GROUP: "group",
5 | ANNOUNCEMENT: "announcement",
6 | ONE_ON_ONE: "1on1",
7 | SELF_DIRECTED: "selfDirected",
8 | PLANNED_ONE_ON_ONE: "planned1on1",
9 | PLANNED_SELF_DIRECTED: "plannedSelfDirected",
10 | } as const;
11 |
12 | export type IndividualProgram = typeof programModeEnumMap[Exclude<
13 | keyof typeof programModeEnumMap,
14 | "GROUP" | "ANNOUNCEMENT"
15 | >];
16 |
17 | type tests = [
18 | Expect<
19 | Equal<
20 | IndividualProgram,
21 | "1on1" | "selfDirected" | "planned1on1" | "plannedSelfDirected"
22 | >
23 | >,
24 | ];
25 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/12-get-object-values.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const frontendToBackendEnumMap = {
4 | singleModule: "SINGLE_MODULE",
5 | multiModule: "MULTI_MODULE",
6 | sharedModule: "SHARED_MODULE",
7 | } as const;
8 |
9 | type BackendModuleEnum = unknown;
10 |
11 | type tests = [
12 | Expect<
13 | Equal
14 | >,
15 | ];
16 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/12-get-object-values.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const frontendToBackendEnumMap = {
4 | singleModule: "SINGLE_MODULE",
5 | multiModule: "MULTI_MODULE",
6 | sharedModule: "SHARED_MODULE",
7 | } as const;
8 |
9 | type BackendModuleEnum =
10 | typeof frontendToBackendEnumMap[keyof typeof frontendToBackendEnumMap];
11 |
12 | type tests = [
13 | Expect<
14 | Equal
15 | >,
16 | ];
17 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/13-get-array-value.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const fruits = ["apple", "banana", "orange"];
4 |
5 | type AppleOrBanana = unknown;
6 | type Fruit = unknown;
7 |
8 | type tests = [
9 | Expect>,
10 | Expect>,
11 | ];
12 |
--------------------------------------------------------------------------------
/src/02-unions-and-indexing/13-get-array-value.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const fruits = ["apple", "banana", "orange"] as const;
4 |
5 | type AppleOrBanana = typeof fruits[0 | 1];
6 | type Fruit = typeof fruits[number];
7 |
8 | type tests = [
9 | Expect>,
10 | Expect>,
11 | ];
12 |
--------------------------------------------------------------------------------
/src/03-template-literals/14-template-literal-with-string.problem.ts:
--------------------------------------------------------------------------------
1 | type Route = unknown;
2 |
3 | export const goToRoute = (route: Route) => {};
4 |
5 | // Should succeed:
6 |
7 | goToRoute("/users");
8 | goToRoute("/");
9 | goToRoute("/admin/users");
10 |
11 | // Should error:
12 |
13 | // @ts-expect-error
14 | goToRoute("users/1");
15 | // @ts-expect-error
16 | goToRoute("http://facebook.com");
17 |
--------------------------------------------------------------------------------
/src/03-template-literals/14-template-literal-with-string.solution.ts:
--------------------------------------------------------------------------------
1 | type Route = `/${string}`;
2 |
3 | export const goToRoute = (route: Route) => {};
4 |
5 | // Should succeed:
6 |
7 | goToRoute("/users");
8 | goToRoute("/");
9 | goToRoute("/admin/users");
10 |
11 | // Should error:
12 |
13 | // @ts-expect-error
14 | goToRoute("users/1");
15 | // @ts-expect-error
16 | goToRoute("http://facebook.com");
17 |
--------------------------------------------------------------------------------
/src/03-template-literals/15-extract-with-template-literals.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Routes = "/users" | "/users/:id" | "/posts" | "/posts/:id";
4 |
5 | type DynamicRoutes = unknown;
6 |
7 | type tests = [Expect>];
8 |
--------------------------------------------------------------------------------
/src/03-template-literals/15-extract-with-template-literals.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Routes = "/users" | "/users/:id" | "/posts" | "/posts/:id";
4 |
5 | type DynamicRoutes = Extract;
6 |
7 | type tests = [Expect>];
8 |
--------------------------------------------------------------------------------
/src/03-template-literals/16-unions-in-template-literals.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type BreadType = "rye" | "brown" | "white";
4 |
5 | type Filling = "cheese" | "ham" | "salami";
6 |
7 | type Sandwich = unknown;
8 |
9 | type tests = [
10 | Expect<
11 | Equal<
12 | Sandwich,
13 | | "rye sandwich with cheese"
14 | | "rye sandwich with ham"
15 | | "rye sandwich with salami"
16 | | "brown sandwich with cheese"
17 | | "brown sandwich with ham"
18 | | "brown sandwich with salami"
19 | | "white sandwich with cheese"
20 | | "white sandwich with ham"
21 | | "white sandwich with salami"
22 | >
23 | >
24 | ];
25 |
--------------------------------------------------------------------------------
/src/03-template-literals/16-unions-in-template-literals.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type BreadType = "rye" | "brown" | "white";
4 |
5 | type Filling = "cheese" | "ham" | "salami";
6 |
7 | type Sandwich = `${BreadType} sandwich with ${Filling}`;
8 |
9 | type tests = [
10 | Expect<
11 | Equal<
12 | Sandwich,
13 | | "rye sandwich with cheese"
14 | | "rye sandwich with ham"
15 | | "rye sandwich with salami"
16 | | "brown sandwich with cheese"
17 | | "brown sandwich with ham"
18 | | "brown sandwich with salami"
19 | | "white sandwich with cheese"
20 | | "white sandwich with ham"
21 | | "white sandwich with salami"
22 | >
23 | >
24 | ];
25 |
--------------------------------------------------------------------------------
/src/03-template-literals/17-splitting-strings.problem.ts:
--------------------------------------------------------------------------------
1 | // Might come in handy!
2 | // import { S } from "ts-toolbelt";
3 | // https://millsp.github.io/ts-toolbelt/modules/string_split.html
4 |
5 | import { Equal, Expect } from "../helpers/type-utils";
6 |
7 | type Path = "Users/John/Documents/notes.txt";
8 |
9 | type SplitPath = unknown;
10 |
11 | type tests = [
12 | Expect>,
13 | ];
14 |
--------------------------------------------------------------------------------
/src/03-template-literals/17-splitting-strings.solution.ts:
--------------------------------------------------------------------------------
1 | import { S } from "ts-toolbelt";
2 | import { Equal, Expect } from "../helpers/type-utils";
3 |
4 | type Path = "Users/John/Documents/notes.txt";
5 |
6 | type SplitPath = S.Split;
7 |
8 | type tests = [
9 | Expect>,
10 | ];
11 |
--------------------------------------------------------------------------------
/src/03-template-literals/18-template-literals-in-object-keys.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type TemplateLiteralKey = `${"user" | "post" | "comment"}${"Id" | "Name"}`;
4 |
5 | type ObjectOfKeys = unknown;
6 |
7 | type tests = [
8 | Expect<
9 | Equal<
10 | ObjectOfKeys,
11 | {
12 | userId: string;
13 | userName: string;
14 | postId: string;
15 | postName: string;
16 | commentId: string;
17 | commentName: string;
18 | }
19 | >
20 | >,
21 | ];
22 |
--------------------------------------------------------------------------------
/src/03-template-literals/18-template-literals-in-object-keys.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type TemplateLiteralKey = `${"user" | "post" | "comment"}${"Id" | "Name"}`;
4 |
5 | type ObjectOfKeys = Record;
6 |
7 | type tests = [
8 | Expect<
9 | Equal<
10 | ObjectOfKeys,
11 | {
12 | userId: string;
13 | userName: string;
14 | postId: string;
15 | postName: string;
16 | commentId: string;
17 | commentName: string;
18 | }
19 | >
20 | >,
21 | ];
22 |
--------------------------------------------------------------------------------
/src/03-template-literals/19-uppercase-object.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Event = `log_in` | "log_out" | "sign_up";
4 |
5 | type ObjectOfKeys = unknown;
6 |
7 | type tests = [
8 | Expect<
9 | Equal<
10 | ObjectOfKeys,
11 | {
12 | LOG_IN: string;
13 | LOG_OUT: string;
14 | SIGN_UP: string;
15 | }
16 | >
17 | >,
18 | ];
19 |
--------------------------------------------------------------------------------
/src/03-template-literals/19-uppercase-object.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Event = `log_in` | "log_out" | "sign_up";
4 |
5 | type ObjectOfKeys = Record, string>;
6 |
7 | type tests = [
8 | Expect<
9 | Equal<
10 | ObjectOfKeys,
11 | {
12 | LOG_IN: string;
13 | LOG_OUT: string;
14 | SIGN_UP: string;
15 | }
16 | >
17 | >
18 | ];
19 |
--------------------------------------------------------------------------------
/src/03.5-type-helpers-pattern/20-type-helpers-pattern.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type ReturnWhatIPassIn = unknown;
4 |
5 | type tests = [
6 | Expect, 1>>,
7 | Expect, "1">>,
8 | Expect, true>>,
9 | Expect, false>>,
10 | Expect, null>>,
11 | ];
12 |
--------------------------------------------------------------------------------
/src/03.5-type-helpers-pattern/20-type-helpers-pattern.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type ReturnWhatIPassIn = T;
4 |
5 | type tests = [
6 | Expect, 1>>,
7 | Expect, "1">>,
8 | Expect, true>>,
9 | Expect, false>>,
10 | Expect, null>>,
11 | ];
12 |
--------------------------------------------------------------------------------
/src/03.5-type-helpers-pattern/20.1-maybe.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Maybe = unknown;
4 |
5 | type tests = [
6 | Expect, string | null | undefined>>,
7 | Expect, number | null | undefined>>,
8 | Expect, boolean | null | undefined>>,
9 | Expect, null | undefined>>,
10 | ];
11 |
--------------------------------------------------------------------------------
/src/03.5-type-helpers-pattern/20.1-maybe.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Maybe = T | null | undefined;
4 |
5 | type tests = [
6 | Expect, string | null | undefined>>,
7 | Expect, number | null | undefined>>,
8 | Expect, boolean | null | undefined>>,
9 | Expect, null | undefined>>,
10 | ];
11 |
--------------------------------------------------------------------------------
/src/03.5-type-helpers-pattern/20.2-constraints.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type AddRoutePrefix = `/${TRoute}`;
4 |
5 | type tests = [
6 | Expect, "/">>,
7 | Expect, "/about">>,
8 | Expect, "/about/team">>,
9 | Expect, "/blog">>,
10 | // @ts-expect-error
11 | AddRoutePrefix,
12 | // @ts-expect-error
13 | AddRoutePrefix,
14 | ];
15 |
--------------------------------------------------------------------------------
/src/03.5-type-helpers-pattern/20.2-constraints.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type AddRoutePrefix = `/${TRoute}`;
4 |
5 | type tests = [
6 | Expect, "/">>,
7 | Expect, "/about">>,
8 | Expect, "/about/team">>,
9 | Expect, "/blog">>,
10 | // @ts-expect-error
11 | AddRoutePrefix,
12 | // @ts-expect-error
13 | AddRoutePrefix,
14 | ];
15 |
--------------------------------------------------------------------------------
/src/03.5-type-helpers-pattern/20.3-multiple.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type CreateDataShape = {
4 | data: unknown;
5 | error: unknown;
6 | };
7 |
8 | type tests = [
9 | Expect<
10 | Equal<
11 | CreateDataShape,
12 | {
13 | data: string;
14 | error: TypeError;
15 | }
16 | >
17 | >,
18 | Expect<
19 | Equal<
20 | CreateDataShape,
21 | {
22 | data: number;
23 | error: Error;
24 | }
25 | >
26 | >,
27 | Expect<
28 | Equal<
29 | CreateDataShape,
30 | {
31 | data: boolean;
32 | error: SyntaxError;
33 | }
34 | >
35 | >,
36 | ];
37 |
--------------------------------------------------------------------------------
/src/03.5-type-helpers-pattern/20.3-multiple.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type CreateDataShape = {
4 | data: TData;
5 | error: TError;
6 | };
7 |
8 | type tests = [
9 | Expect<
10 | Equal<
11 | CreateDataShape,
12 | {
13 | data: string;
14 | error: TypeError;
15 | }
16 | >
17 | >,
18 | Expect<
19 | Equal<
20 | CreateDataShape,
21 | {
22 | data: number;
23 | error: Error;
24 | }
25 | >
26 | >,
27 | Expect<
28 | Equal<
29 | CreateDataShape,
30 | {
31 | data: boolean;
32 | error: SyntaxError;
33 | }
34 | >
35 | >,
36 | ];
37 |
--------------------------------------------------------------------------------
/src/03.5-type-helpers-pattern/20.4-defaults.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type CreateDataShape = {
4 | data: TData;
5 | error: TError;
6 | };
7 |
8 | type tests = [
9 | Expect<
10 | Equal<
11 | CreateDataShape,
12 | {
13 | data: string;
14 | error: undefined;
15 | }
16 | >
17 | >,
18 | Expect<
19 | Equal<
20 | CreateDataShape,
21 | {
22 | data: boolean;
23 | error: SyntaxError;
24 | }
25 | >
26 | >,
27 | ];
28 |
--------------------------------------------------------------------------------
/src/03.5-type-helpers-pattern/20.4-defaults.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type CreateDataShape = {
4 | data: TData;
5 | error: TError;
6 | };
7 |
8 | type tests = [
9 | Expect<
10 | Equal<
11 | CreateDataShape,
12 | {
13 | data: string;
14 | error: undefined;
15 | }
16 | >
17 | >,
18 | Expect<
19 | Equal<
20 | CreateDataShape,
21 | {
22 | data: boolean;
23 | error: SyntaxError;
24 | }
25 | >
26 | >,
27 | ];
28 |
--------------------------------------------------------------------------------
/src/03.5-type-helpers-pattern/20.5-function-constraints.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type GetParametersAndReturnType = {
4 | params: Parameters;
5 | returnValue: ReturnType;
6 | };
7 |
8 | type tests = [
9 | Expect<
10 | Equal<
11 | GetParametersAndReturnType<() => string>,
12 | { params: []; returnValue: string }
13 | >
14 | >,
15 | Expect<
16 | Equal<
17 | GetParametersAndReturnType<(s: string) => void>,
18 | { params: [string]; returnValue: void }
19 | >
20 | >,
21 | Expect<
22 | Equal<
23 | GetParametersAndReturnType<(n: number, b: boolean) => number>,
24 | { params: [number, boolean]; returnValue: number }
25 | >
26 | >,
27 | ];
28 |
--------------------------------------------------------------------------------
/src/03.5-type-helpers-pattern/20.5-function-constraints.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type GetParametersAndReturnType any> = {
4 | params: Parameters;
5 | returnValue: ReturnType;
6 | };
7 |
8 | type tests = [
9 | Expect<
10 | Equal<
11 | GetParametersAndReturnType<() => string>,
12 | { params: []; returnValue: string }
13 | >
14 | >,
15 | Expect<
16 | Equal<
17 | GetParametersAndReturnType<(s: string) => void>,
18 | { params: [string]; returnValue: void }
19 | >
20 | >,
21 | Expect<
22 | Equal<
23 | GetParametersAndReturnType<(n: number, b: boolean) => number>,
24 | { params: [number, boolean]; returnValue: number }
25 | >
26 | >,
27 | ];
28 |
--------------------------------------------------------------------------------
/src/03.5-type-helpers-pattern/20.6-not-undefined-or-null-constraint.problem.ts:
--------------------------------------------------------------------------------
1 | export type Maybe = T | null | undefined;
2 |
3 | type tests = [
4 | // @ts-expect-error
5 | Maybe,
6 | // @ts-expect-error
7 | Maybe,
8 |
9 | Maybe,
10 | Maybe,
11 | Maybe<0>,
12 | Maybe<"">,
13 | ];
14 |
--------------------------------------------------------------------------------
/src/03.5-type-helpers-pattern/20.6-not-undefined-or-null-constraint.solution.ts:
--------------------------------------------------------------------------------
1 | export type Maybe = T | null | undefined;
2 |
3 | type tests = [
4 | // @ts-expect-error
5 | Maybe,
6 | // @ts-expect-error
7 | Maybe,
8 |
9 | Maybe,
10 | Maybe,
11 | Maybe<0>,
12 | Maybe<"">,
13 | ];
14 |
--------------------------------------------------------------------------------
/src/03.5-type-helpers-pattern/20.7-non-empty-array.problem.ts:
--------------------------------------------------------------------------------
1 | type NonEmptyArray = unknown;
2 |
3 | export const makeEnum = (values: NonEmptyArray) => {};
4 |
5 | makeEnum(["a"]);
6 | makeEnum(["a", "b", "c"]);
7 |
8 | // @ts-expect-error
9 | makeEnum([]);
10 |
--------------------------------------------------------------------------------
/src/03.5-type-helpers-pattern/20.7-non-empty-array.solution.ts:
--------------------------------------------------------------------------------
1 | type NonEmptyArray = [T, ...Array];
2 |
3 | export const makeEnum = (values: NonEmptyArray) => {};
4 |
5 | makeEnum(["a"]);
6 | makeEnum(["a", "b", "c"]);
7 |
8 | // @ts-expect-error
9 | makeEnum([]);
10 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/21-conditional-types.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type YouSayGoodbyeAndISayHello = unknown;
4 |
5 | type tests = [
6 | Expect, "goodbye">>,
7 | Expect, "hello">>,
8 | ];
9 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/21-conditional-types.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type YouSayGoodbyeAndISayHello = T extends "hello" ? "goodbye" : "hello";
4 |
5 | type tests = [
6 | Expect, "goodbye">>,
7 | Expect, "hello">>,
8 | ];
9 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/22-returning-never.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type YouSayGoodbyeAndISayHello = T extends "hello" ? "goodbye" : "hello";
4 |
5 | type tests = [
6 | Expect, "goodbye">>,
7 | Expect, "hello">>,
8 | Expect, never>>,
9 | Expect, never>>,
10 | ];
11 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/22-returning-never.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type YouSayGoodbyeAndISayHello = T extends "hello" | "goodbye"
4 | ? T extends "hello"
5 | ? "goodbye"
6 | : "hello"
7 | : never;
8 |
9 | type tests = [
10 | Expect, "goodbye">>,
11 | Expect, "hello">>,
12 | Expect, never>>,
13 | Expect, never>>,
14 | ];
15 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/22.5-why-conditional-types-are-needed.explainer.ts:
--------------------------------------------------------------------------------
1 | // Daniel Rosenwasser explains why conditional types were needed in TS
2 |
3 | export {};
4 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/23-infer-with-raw-values.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type GetDataValue = unknown;
4 |
5 | type tests = [
6 | Expect, "hello">>,
7 | Expect, { name: "hello" }>>,
8 | Expect<
9 | Equal<
10 | GetDataValue<{ data: { name: "hello"; age: 20 } }>,
11 | { name: "hello"; age: 20 }
12 | >
13 | >,
14 | // Expect that if you pass in string, it
15 | // should return never
16 | Expect, never>>,
17 | ];
18 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/23-infer-with-raw-values.solution.1.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type GetDataValue = T extends { data: any } ? T["data"] : never;
4 |
5 | type tests = [
6 | Expect, "hello">>,
7 | Expect, { name: "hello" }>>,
8 | Expect<
9 | Equal<
10 | GetDataValue<{ data: { name: "hello"; age: 20 } }>,
11 | { name: "hello"; age: 20 }
12 | >
13 | >,
14 | // Expect that if you pass in string, it
15 | // should return never
16 | Expect, never>>,
17 | ];
18 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/23-infer-with-raw-values.solution.2.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type GetDataValue = T extends { data: infer TInferredData }
4 | ? TInferredData
5 | : never;
6 |
7 | type tests = [
8 | Expect, "hello">>,
9 | Expect, { name: "hello" }>>,
10 | Expect<
11 | Equal<
12 | GetDataValue<{ data: { name: "hello"; age: 20 } }>,
13 | { name: "hello"; age: 20 }
14 | >
15 | >,
16 | // Expect that if you pass in string, it
17 | // should return never
18 | Expect, never>>,
19 | ];
20 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/24-infer-with-generics.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | interface MyComplexInterface {
4 | getEvent: () => Event;
5 | getContext: () => Context;
6 | getName: () => Name;
7 | getPoint: () => Point;
8 | }
9 |
10 | type Example = MyComplexInterface<
11 | "click",
12 | "window",
13 | "my-event",
14 | { x: 12; y: 14 }
15 | >;
16 |
17 | type GetPoint = unknown;
18 |
19 | type tests = [Expect, { x: 12; y: 14 }>>];
20 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/24-infer-with-generics.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | interface MyComplexInterface {
4 | getEvent: () => Event;
5 | getContext: () => Context;
6 | getName: () => Name;
7 | getPoint: () => Point;
8 | }
9 |
10 | type Example = MyComplexInterface<
11 | "click",
12 | "window",
13 | "my-event",
14 | { x: 12; y: 14 }
15 | >;
16 |
17 | type GetPoint = T extends MyComplexInterface
18 | ? TPoint
19 | : never;
20 |
21 | type tests = [Expect, { x: 12; y: 14 }>>];
22 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/25-template-literal-value-extraction.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Names = [
4 | "Matt Pocock",
5 | "Jimi Hendrix",
6 | "Eric Clapton",
7 | "John Mayer",
8 | "BB King",
9 | ];
10 |
11 | type GetSurname = unknown;
12 |
13 | type tests = [
14 | Expect, "Pocock">>,
15 | Expect, "Hendrix">>,
16 | Expect, "Clapton">>,
17 | Expect, "Mayer">>,
18 | Expect, "King">>,
19 | ];
20 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/25-template-literal-value-extraction.solution.1.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Names = [
4 | "Matt Pocock",
5 | "Jimi Hendrix",
6 | "Eric Clapton",
7 | "John Mayer",
8 | "BB King",
9 | ];
10 |
11 | type GetSurname = T extends `${infer First} ${infer Last}` ? Last : never;
12 |
13 | type tests = [
14 | Expect, "Pocock">>,
15 | Expect, "Hendrix">>,
16 | Expect, "Clapton">>,
17 | Expect, "Mayer">>,
18 | Expect, "King">>,
19 | ];
20 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/25-template-literal-value-extraction.solution.2.ts:
--------------------------------------------------------------------------------
1 | import { S } from "ts-toolbelt";
2 | import { Equal, Expect } from "../helpers/type-utils";
3 |
4 | type Names = [
5 | "Matt Pocock",
6 | "Jimi Hendrix",
7 | "Eric Clapton",
8 | "John Mayer",
9 | "BB King",
10 | ];
11 |
12 | /**
13 | * This is an alternative way of doing it, using S.Split
14 | */
15 | type GetSurname = S.Split[1];
16 |
17 | type tests = [
18 | Expect, "Pocock">>,
19 | Expect, "Hendrix">>,
20 | Expect, "Clapton">>,
21 | Expect, "Mayer">>,
22 | Expect, "King">>,
23 | ];
24 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/25.5-regex-types.explainer.ts:
--------------------------------------------------------------------------------
1 | // Daniel Rosenwasser explains why regex types WEREN'T added to TypeScript
2 |
3 | export {};
4 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/26-get-result-from-async-function.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const getServerSideProps = async () => {
4 | const data = await fetch("https://jsonplaceholder.typicode.com/todos/1");
5 | const json: { title: string } = await data.json();
6 | return {
7 | props: {
8 | json,
9 | },
10 | };
11 | };
12 |
13 | type InferPropsFromServerSideFunction = unknown;
14 |
15 | type tests = [
16 | Expect<
17 | Equal<
18 | InferPropsFromServerSideFunction,
19 | { json: { title: string } }
20 | >
21 | >
22 | ];
23 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/26-get-result-from-async-function.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const getServerSideProps = async () => {
4 | const data = await fetch("https://jsonplaceholder.typicode.com/todos/1");
5 | const json: { title: string } = await data.json();
6 | return {
7 | props: {
8 | json,
9 | },
10 | };
11 | };
12 |
13 | type InferPropsFromServerSideFunction = T extends () => Promise<{
14 | props: infer P;
15 | }>
16 | ? P
17 | : never;
18 |
19 | type tests = [
20 | Expect<
21 | Equal<
22 | InferPropsFromServerSideFunction,
23 | { json: { title: string } }
24 | >
25 | >
26 | ];
27 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/27-infer-in-union-types.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const parser1 = {
4 | parse: () => 1,
5 | };
6 |
7 | const parser2 = () => "123";
8 |
9 | const parser3 = {
10 | extract: () => true,
11 | };
12 |
13 | type GetParserResult = unknown;
14 |
15 | type tests = [
16 | Expect, number>>,
17 | Expect, string>>,
18 | Expect, boolean>>,
19 | ];
20 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/27-infer-in-union-types.solution.1.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const parser1 = {
4 | parse: () => 1,
5 | };
6 |
7 | const parser2 = () => "123";
8 |
9 | const parser3 = {
10 | extract: () => true,
11 | };
12 |
13 | type GetParserResult = T extends {
14 | parse: () => infer TResult;
15 | }
16 | ? TResult
17 | : T extends () => infer TResult
18 | ? TResult
19 | : T extends {
20 | extract: () => infer TResult;
21 | }
22 | ? TResult
23 | : never;
24 |
25 | type tests = [
26 | Expect, number>>,
27 | Expect, string>>,
28 | Expect, boolean>>,
29 | ];
30 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/27-infer-in-union-types.solution.2.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | const parser1 = {
4 | parse: () => 1,
5 | };
6 |
7 | const parser2 = () => "123";
8 |
9 | const parser3 = {
10 | extract: () => true,
11 | };
12 |
13 | type GetParserResult = T extends
14 | | {
15 | parse: () => infer TResult;
16 | }
17 | | {
18 | extract: () => infer TResult;
19 | }
20 | | (() => infer TResult)
21 | ? TResult
22 | : never;
23 |
24 | type tests = [
25 | Expect, number>>,
26 | Expect, string>>,
27 | Expect, boolean>>,
28 | ];
29 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/28-distributive-conditional-types.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Fruit = "apple" | "banana" | "orange";
4 |
5 | type AppleOrBanana = Fruit extends "apple" | "banana" ? Fruit : never;
6 |
7 | type tests = [Expect>];
8 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/28-distributive-conditional-types.solution.1.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Fruit = "apple" | "banana" | "orange";
4 |
5 | type AppleOrBanana = Fruit extends infer T
6 | ? T extends "apple" | "banana"
7 | ? T
8 | : never
9 | : never;
10 |
11 | type tests = [Expect>];
12 |
--------------------------------------------------------------------------------
/src/04-conditional-types-and-infer/28-distributive-conditional-types.solution.2.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Fruit = "apple" | "banana" | "orange";
4 |
5 | type GetAppleOrBanana = T extends "apple" | "banana" ? T : never;
6 |
7 | type AppleOrBanana = GetAppleOrBanana;
8 |
9 | type tests = [Expect>];
10 |
--------------------------------------------------------------------------------
/src/05-key-remapping/29-union-to-object.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Route = "/" | "/about" | "/admin" | "/admin/users";
4 |
5 | type RoutesObject = unknown;
6 |
7 | type tests = [
8 | Expect<
9 | Equal<
10 | RoutesObject,
11 | {
12 | "/": "/";
13 | "/about": "/about";
14 | "/admin": "/admin";
15 | "/admin/users": "/admin/users";
16 | }
17 | >
18 | >,
19 | ];
20 |
--------------------------------------------------------------------------------
/src/05-key-remapping/29-union-to-object.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Route = "/" | "/about" | "/admin" | "/admin/users";
4 |
5 | type RoutesObject = {
6 | [R in Route]: R;
7 | };
8 |
9 | type tests = [
10 | Expect<
11 | Equal<
12 | RoutesObject,
13 | {
14 | "/": "/";
15 | "/about": "/about";
16 | "/admin": "/admin";
17 | "/admin/users": "/admin/users";
18 | }
19 | >
20 | >,
21 | ];
22 |
--------------------------------------------------------------------------------
/src/05-key-remapping/30-k-in-keyof.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | interface Attributes {
4 | firstName: string;
5 | lastName: string;
6 | age: number;
7 | }
8 |
9 | type AttributeGetters = unknown;
10 |
11 | type tests = [
12 | Expect<
13 | Equal<
14 | AttributeGetters,
15 | {
16 | firstName: () => string;
17 | lastName: () => string;
18 | age: () => number;
19 | }
20 | >
21 | >,
22 | ];
23 |
--------------------------------------------------------------------------------
/src/05-key-remapping/30-k-in-keyof.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | interface Attributes {
4 | firstName: string;
5 | lastName: string;
6 | age: number;
7 | }
8 |
9 | type AttributeGetters = {
10 | [K in keyof Attributes]: () => Attributes[K];
11 | };
12 |
13 | type tests = [
14 | Expect<
15 | Equal<
16 | AttributeGetters,
17 | {
18 | firstName: () => string;
19 | lastName: () => string;
20 | age: () => number;
21 | }
22 | >
23 | >,
24 | ];
25 |
--------------------------------------------------------------------------------
/src/05-key-remapping/31-k-in-keyof-as.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | interface Attributes {
4 | firstName: string;
5 | lastName: string;
6 | age: number;
7 | }
8 |
9 | type AttributeGetters = {
10 | [K in keyof Attributes]: () => Attributes[K];
11 | };
12 |
13 | type tests = [
14 | Expect<
15 | Equal<
16 | AttributeGetters,
17 | {
18 | getFirstName: () => string;
19 | getLastName: () => string;
20 | getAge: () => number;
21 | }
22 | >
23 | >
24 | ];
25 |
--------------------------------------------------------------------------------
/src/05-key-remapping/31-k-in-keyof-as.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | interface Attributes {
4 | firstName: string;
5 | lastName: string;
6 | age: number;
7 | }
8 |
9 | type AttributeGetters = {
10 | [K in keyof Attributes as `get${Capitalize}`]: () => Attributes[K];
11 | };
12 |
13 | type tests = [
14 | Expect<
15 | Equal<
16 | AttributeGetters,
17 | {
18 | getFirstName: () => string;
19 | getLastName: () => string;
20 | getAge: () => number;
21 | }
22 | >
23 | >,
24 | ];
25 |
--------------------------------------------------------------------------------
/src/05-key-remapping/32-never-in-key-remapping.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | interface Example {
4 | name: string;
5 | age: number;
6 | id: string;
7 | organisationId: string;
8 | groupId: string;
9 | }
10 |
11 | type OnlyIdKeys = unknown;
12 |
13 | type tests = [
14 | Expect<
15 | Equal<
16 | OnlyIdKeys,
17 | {
18 | id: string;
19 | organisationId: string;
20 | groupId: string;
21 | }
22 | >
23 | >,
24 | Expect, {}>>
25 | ];
26 |
--------------------------------------------------------------------------------
/src/05-key-remapping/32-never-in-key-remapping.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | interface Example {
4 | name: string;
5 | age: number;
6 | id: string;
7 | organisationId: string;
8 | groupId: string;
9 | }
10 |
11 | type OnlyIdKeys = {
12 | [K in keyof T as K extends `${string}${"id" | "Id"}${string}`
13 | ? K
14 | : never]: T[K];
15 | };
16 |
17 | type tests = [
18 | Expect<
19 | Equal<
20 | OnlyIdKeys,
21 | {
22 | id: string;
23 | organisationId: string;
24 | groupId: string;
25 | }
26 | >
27 | >,
28 | Expect, {}>>
29 | ];
30 |
--------------------------------------------------------------------------------
/src/05-key-remapping/33-discriminated-union-to-object.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Route =
4 | | {
5 | route: "/";
6 | search: {
7 | page: string;
8 | perPage: string;
9 | };
10 | }
11 | | { route: "/about"; search: {} }
12 | | { route: "/admin"; search: {} }
13 | | { route: "/admin/users"; search: {} };
14 |
15 | type RoutesObject = unknown;
16 |
17 | type tests = [
18 | Expect<
19 | Equal<
20 | RoutesObject,
21 | {
22 | "/": {
23 | page: string;
24 | perPage: string;
25 | };
26 | "/about": {};
27 | "/admin": {};
28 | "/admin/users": {};
29 | }
30 | >
31 | >,
32 | ];
33 |
--------------------------------------------------------------------------------
/src/05-key-remapping/33-discriminated-union-to-object.solution.1.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Route =
4 | | {
5 | route: "/";
6 | search: {
7 | page: string;
8 | perPage: string;
9 | };
10 | }
11 | | { route: "/about"; search: {} }
12 | | { route: "/admin"; search: {} }
13 | | { route: "/admin/users"; search: {} };
14 |
15 | /**
16 | * This is useful, but less powerful than solution 2:
17 | */
18 | type RoutesObject = {
19 | [R in Route["route"]]: Extract["search"];
20 | };
21 |
22 | type tests = [
23 | Expect<
24 | Equal<
25 | RoutesObject,
26 | {
27 | "/": {
28 | page: string;
29 | perPage: string;
30 | };
31 | "/about": {};
32 | "/admin": {};
33 | "/admin/users": {};
34 | }
35 | >
36 | >,
37 | ];
38 |
--------------------------------------------------------------------------------
/src/05-key-remapping/33-discriminated-union-to-object.solution.2.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Route =
4 | | {
5 | route: "/";
6 | search: {
7 | page: string;
8 | perPage: string;
9 | };
10 | }
11 | | { route: "/about"; search: {} }
12 | | { route: "/admin"; search: {} }
13 | | { route: "/admin/users"; search: {} };
14 |
15 | /**
16 | * Here, R represents the individual Route. The lesson here
17 | * is that the thing we're iterating DOESN'T have to be a
18 | * string | number | symbol, as long as the thing we cast it to
19 | * is.
20 | */
21 | type RoutesObject = {
22 | [R in Route as R["route"]]: R["search"];
23 | };
24 |
25 | type tests = [
26 | Expect<
27 | Equal<
28 | RoutesObject,
29 | {
30 | "/": {
31 | page: string;
32 | perPage: string;
33 | };
34 | "/about": {};
35 | "/admin": {};
36 | "/admin/users": {};
37 | }
38 | >
39 | >,
40 | ];
41 |
--------------------------------------------------------------------------------
/src/05-key-remapping/34-object-to-union-of-tuples.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | interface Values {
4 | email: string;
5 | firstName: string;
6 | lastName: string;
7 | }
8 |
9 | type ValuesAsUnionOfTuples = {
10 | [K in keyof Values]: [K, Values[K]];
11 | };
12 |
13 | type tests = [
14 | Expect<
15 | Equal<
16 | ValuesAsUnionOfTuples,
17 | ["email", string] | ["firstName", string] | ["lastName", string]
18 | >
19 | >
20 | ];
21 |
--------------------------------------------------------------------------------
/src/05-key-remapping/34-object-to-union-of-tuples.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | interface Values {
4 | email: string;
5 | firstName: string;
6 | lastName: string;
7 | }
8 |
9 | type ValuesAsUnionOfTuples = {
10 | [V in keyof Values]: [V, Values[V]];
11 | }[keyof Values];
12 |
13 | type tests = [
14 | Expect<
15 | Equal<
16 | ValuesAsUnionOfTuples,
17 | ["email", string] | ["firstName", string] | ["lastName", string]
18 | >
19 | >,
20 | ];
21 |
--------------------------------------------------------------------------------
/src/05-key-remapping/35-object-to-union-of-template-literals.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | interface FruitMap {
4 | apple: "red";
5 | banana: "yellow";
6 | orange: "orange";
7 | }
8 |
9 | type TransformedFruit = unknown;
10 |
11 | type tests = [
12 | Expect<
13 | Equal
14 | >,
15 | ];
16 |
--------------------------------------------------------------------------------
/src/05-key-remapping/35-object-to-union-of-template-literals.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | interface FruitMap {
4 | apple: "red";
5 | banana: "yellow";
6 | orange: "orange";
7 | }
8 |
9 | type TransformedFruit = {
10 | [F in keyof FruitMap]: `${F}:${FruitMap[F]}`;
11 | }[keyof FruitMap];
12 |
13 | type tests = [
14 | Expect<
15 | Equal
16 | >,
17 | ];
18 |
--------------------------------------------------------------------------------
/src/05-key-remapping/36-discriminated-union-to-union.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Fruit =
4 | | {
5 | name: "apple";
6 | color: "red";
7 | }
8 | | {
9 | name: "banana";
10 | color: "yellow";
11 | }
12 | | {
13 | name: "orange";
14 | color: "orange";
15 | };
16 |
17 | type TransformedFruit = unknown;
18 |
19 | type tests = [
20 | Expect<
21 | Equal
22 | >,
23 | ];
24 |
--------------------------------------------------------------------------------
/src/05-key-remapping/36-discriminated-union-to-union.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Fruit =
4 | | {
5 | name: "apple";
6 | color: "red";
7 | }
8 | | {
9 | name: "banana";
10 | color: "yellow";
11 | }
12 | | {
13 | name: "orange";
14 | color: "orange";
15 | };
16 |
17 | type TransformedFruit = {
18 | [F in Fruit as F["name"]]: `${F["name"]}:${F["color"]}`;
19 | }[Fruit["name"]];
20 |
21 | type tests = [
22 | Expect<
23 | Equal
24 | >
25 | ];
26 |
--------------------------------------------------------------------------------
/src/06-challenges/37-get-dynamic-path-params.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type UserPath = "/users/:id";
4 |
5 | type UserOrganisationPath = "/users/:id/organisations/:organisationId";
6 |
7 | type ExtractPathParams = unknown;
8 |
9 | type tests = [
10 | Expect, { id: string }>>,
11 | Expect<
12 | Equal<
13 | ExtractPathParams,
14 | { id: string; organisationId: string }
15 | >
16 | >,
17 | ];
18 |
--------------------------------------------------------------------------------
/src/06-challenges/37-get-dynamic-path-params.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 | import { S } from "ts-toolbelt";
3 |
4 | type UserPath = "/users/:id";
5 |
6 | type UserOrganisationPath = "/users/:id/organisations/:organisationId";
7 |
8 | type ExtractPathParams = {
9 | [K in S.Split[number] as K extends `:${infer P}`
10 | ? P
11 | : never]: string;
12 | };
13 |
14 | type tests = [
15 | Expect, { id: string }>>,
16 | Expect<
17 | Equal<
18 | ExtractPathParams,
19 | { id: string; organisationId: string }
20 | >
21 | >,
22 | ];
23 |
--------------------------------------------------------------------------------
/src/06-challenges/38-mutually-exclusive-properties.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | interface Attributes {
4 | id: string;
5 | email: string;
6 | username: string;
7 | }
8 |
9 | /**
10 | * How do we create a type helper that represents a union
11 | * of all possible combinations of Attributes?
12 | */
13 | type MutuallyExclusive = unknown;
14 |
15 | type ExclusiveAttributes = MutuallyExclusive;
16 |
17 | type tests = [
18 | Expect<
19 | Equal<
20 | ExclusiveAttributes,
21 | | {
22 | id: string;
23 | }
24 | | {
25 | email: string;
26 | }
27 | | {
28 | username: string;
29 | }
30 | >
31 | >,
32 | ];
33 |
--------------------------------------------------------------------------------
/src/06-challenges/38-mutually-exclusive-properties.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | interface Attributes {
4 | id: string;
5 | email: string;
6 | username: string;
7 | }
8 |
9 | type MutuallyExclusive = {
10 | [K in keyof T]: Record;
11 | }[keyof T];
12 |
13 | type ExclusiveAttributes = MutuallyExclusive;
14 |
15 | type tests = [
16 | Expect<
17 | Equal<
18 | ExclusiveAttributes,
19 | | {
20 | id: string;
21 | }
22 | | {
23 | email: string;
24 | }
25 | | {
26 | username: string;
27 | }
28 | >
29 | >,
30 | ];
31 |
--------------------------------------------------------------------------------
/src/06-challenges/39-discriminated-union-with-unique-values-to-object.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Route =
4 | | {
5 | route: "/";
6 | search: {
7 | page: string;
8 | perPage: string;
9 | };
10 | }
11 | | { route: "/about" }
12 | | { route: "/admin" }
13 | | { route: "/admin/users" };
14 |
15 | type RoutesObject = unknown;
16 |
17 | type tests = [
18 | Expect<
19 | Equal<
20 | RoutesObject,
21 | {
22 | "/": {
23 | page: string;
24 | perPage: string;
25 | };
26 | "/about": never;
27 | "/admin": never;
28 | "/admin/users": never;
29 | }
30 | >
31 | >,
32 | ];
33 |
--------------------------------------------------------------------------------
/src/06-challenges/39-discriminated-union-with-unique-values-to-object.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type Route =
4 | | {
5 | route: "/";
6 | search: {
7 | page: string;
8 | perPage: string;
9 | };
10 | }
11 | | { route: "/about" }
12 | | { route: "/admin" }
13 | | { route: "/admin/users" };
14 |
15 | type RoutesObject = {
16 | [R in Route as R["route"]]: R extends { search: infer S } ? S : never;
17 | };
18 |
19 | type tests = [
20 | Expect<
21 | Equal<
22 | RoutesObject,
23 | {
24 | "/": {
25 | page: string;
26 | perPage: string;
27 | };
28 | "/about": never;
29 | "/admin": never;
30 | "/admin/users": never;
31 | }
32 | >
33 | >
34 | ];
35 |
--------------------------------------------------------------------------------
/src/06-challenges/40-deep-partial.problem.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type DeepPartial = unknown;
4 |
5 | type MyType = {
6 | a: string;
7 | b: number;
8 | c: {
9 | d: string;
10 | e: {
11 | f: string;
12 | g: {
13 | h: string;
14 | i: string;
15 | }[];
16 | };
17 | };
18 | };
19 |
20 | type Result = DeepPartial;
21 |
22 | type tests = [
23 | Expect<
24 | Equal<
25 | Result,
26 | {
27 | a?: string;
28 | b?: number;
29 | c?: {
30 | d?: string;
31 | e?: {
32 | f?: string;
33 | g?: {
34 | h?: string;
35 | i?: string;
36 | }[];
37 | };
38 | };
39 | }
40 | >
41 | >
42 | ];
43 |
--------------------------------------------------------------------------------
/src/06-challenges/40-deep-partial.solution.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type DeepPartial = T extends Array
4 | ? Array>
5 | : { [K in keyof T]?: DeepPartial };
6 |
7 | type MyType = {
8 | a: string;
9 | b: number;
10 | c: {
11 | d: string;
12 | e: {
13 | f: string;
14 | g: {
15 | h: string;
16 | i: string;
17 | }[];
18 | };
19 | };
20 | };
21 |
22 | type Result = DeepPartial;
23 |
24 | type tests = [
25 | Expect<
26 | Equal<
27 | Result,
28 | {
29 | a?: string;
30 | b?: number;
31 | c?: {
32 | d?: string;
33 | e?: {
34 | f?: string;
35 | g?: {
36 | h?: string;
37 | i?: string;
38 | }[];
39 | };
40 | };
41 | }
42 | >
43 | >,
44 | ];
45 |
--------------------------------------------------------------------------------
/src/helpers/type-utils.ts:
--------------------------------------------------------------------------------
1 | export type Expect = T;
2 |
3 | export type Equal = (() => T extends X ? 1 : 2) extends <
4 | T,
5 | >() => T extends Y ? 1 : 2
6 | ? true
7 | : false;
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "types": [
4 | "vitest/importMeta"
5 | ],
6 | "target": "es2020",
7 | "module": "ES2022",
8 | "moduleResolution": "node",
9 | "noEmit": true,
10 | "isolatedModules": true,
11 | "esModuleInterop": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "strict": true,
14 | "skipLibCheck": true
15 | },
16 | "include": [
17 | "src"
18 | ]
19 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------