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