├── .github └── workflows │ ├── renovate-checks.yml │ └── section-repos.yml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── og-image.png ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── 01-number.problem.ts ├── 01-number.solution.ts ├── 02-object-param.problem.ts ├── 02-object-param.solution.1.ts ├── 02-object-param.solution.2.ts ├── 02-object-param.solution.3.ts ├── 03-optional-properties.problem.ts ├── 03-optional-properties.solution.ts ├── 04-optional-params.problem.ts ├── 04-optional-params.solution.ts ├── 05-assigning-types-to-variables.problem.ts ├── 05-assigning-types-to-variables.solution.ts ├── 06-unions.problem.ts ├── 06-unions.solution.ts ├── 07-arrays.problem.ts ├── 07-arrays.solution.1.ts ├── 07-arrays.solution.2.ts ├── 08-function-return-type-annotations.problem.ts ├── 08-function-return-type-annotations.solution.ts ├── 09-promises.problem.ts ├── 09-promises.solution.1.ts ├── 09-promises.solution.2.ts ├── 09-promises.solution.3.ts ├── 10-set.problem.ts ├── 10-set.solution.ts ├── 11-record.problem.ts ├── 11-record.solution.1.ts ├── 11-record.solution.2.ts ├── 11-record.solution.3.ts ├── 12-typeof-narrowing.problem.ts ├── 12-typeof-narrowing.solution.ts ├── 13-catch-blocks.problem.ts ├── 13-catch-blocks.solution.1.ts ├── 13-catch-blocks.solution.2.ts ├── 13-catch-blocks.solution.3.ts ├── 14-extends.problem.ts ├── 14-extends.solution.ts ├── 15-intersection.problem.ts ├── 15-intersection.solution.ts ├── 16-omit-and-pick.problem.ts ├── 16-omit-and-pick.solution.1.ts ├── 16-omit-and-pick.solution.2.ts ├── 17-function-types.problem.ts ├── 17-function-types.solution.1.ts ├── 17-function-types.solution.2.ts ├── 18-function-types-with-promises.problem.ts ├── 18-function-types-with-promises.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 | beginner typescript tutorial 2 | 3 | ## Quickstart 4 | 5 | Take the course on [Total TypeScript](https://totaltypescript.com/tutorials/beginners-typescript). There, you'll find: 6 | 7 | - Video explanations for each problem and solution 8 | - Transcripts 9 | - Text explanations 10 | - A built-in Stackblitz editor 11 | 12 | ```sh 13 | # Installs all dependencies 14 | npm install 15 | 16 | # Asks you which exercise you'd like to run, and runs it 17 | npm run exercise 18 | ``` 19 | 20 | ## How to take the course 21 | 22 | You'll notice that the course is split into exercises. Each exercise is split into a `*.problem` and a `*.solution`. 23 | 24 | To take an exercise: 25 | 26 | 1. Run `npm run exercise` 27 | 2. Choose which exercise you'd like to run. 28 | 29 | 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: 30 | 31 | 1. Check out [TypeScript's docs](https://www.typescriptlang.org/docs/handbook/intro.html). 32 | 1. Try to find something that looks relevant. 33 | 1. Give it a go to see if it solves the problem. 34 | 35 | You'll know if you've succeeded because the tests will pass. 36 | 37 | **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! 38 | 39 | ## Acknowledgements 40 | 41 | 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). 42 | 43 | ## Reference 44 | 45 | ### `npm run exercise` 46 | 47 | Alias: `npm run e` 48 | 49 | Open a prompt for choosing which exercise you'd like to run. 50 | -------------------------------------------------------------------------------- /og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/total-typescript/beginners-typescript-tutorial/e430c2da7ab0043c39b1b14a7731a27f1677467e/og-image.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beginners-typescript", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "main": "index.js", 6 | "author": "Matt Pocock ", 7 | "license": "GPL-3.0", 8 | "devDependencies": { 9 | "@total-typescript/exercise-cli": "^0.11.0", 10 | "@total-typescript/tsconfig": "^1.0.3", 11 | "@types/node": "^18.6.5", 12 | "cross-fetch": "^3.1.5", 13 | "jsdom": "^25.0.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 | "prepare": "tt-cli prepare-stackblitz", 24 | "e-01": "tt-cli run 01", 25 | "s-01": "tt-cli run 01 --solution", 26 | "e-02": "tt-cli run 02", 27 | "s-02": "tt-cli run 02 --solution", 28 | "e-03": "tt-cli run 03", 29 | "s-03": "tt-cli run 03 --solution", 30 | "e-04": "tt-cli run 04", 31 | "s-04": "tt-cli run 04 --solution", 32 | "e-05": "tt-cli run 05", 33 | "s-05": "tt-cli run 05 --solution", 34 | "e-06": "tt-cli run 06", 35 | "s-06": "tt-cli run 06 --solution", 36 | "e-07": "tt-cli run 07", 37 | "s-07": "tt-cli run 07 --solution", 38 | "e-08": "tt-cli run 08", 39 | "s-08": "tt-cli run 08 --solution", 40 | "e-09": "tt-cli run 09", 41 | "s-09": "tt-cli run 09 --solution", 42 | "e-10": "tt-cli run 10", 43 | "s-10": "tt-cli run 10 --solution", 44 | "e-11": "tt-cli run 11", 45 | "s-11": "tt-cli run 11 --solution", 46 | "e-12": "tt-cli run 12", 47 | "s-12": "tt-cli run 12 --solution", 48 | "e-13": "tt-cli run 13", 49 | "s-13": "tt-cli run 13 --solution", 50 | "e-14": "tt-cli run 14", 51 | "s-14": "tt-cli run 14 --solution", 52 | "e-15": "tt-cli run 15", 53 | "s-15": "tt-cli run 15 --solution", 54 | "e-16": "tt-cli run 16", 55 | "s-16": "tt-cli run 16 --solution", 56 | "e-17": "tt-cli run 17", 57 | "s-17": "tt-cli run 17 --solution", 58 | "e-18": "tt-cli run 18", 59 | "s-18": "tt-cli run 18 --solution" 60 | }, 61 | "dependencies": { 62 | "@types/express": "^4.17.13", 63 | "express": "^4.18.1", 64 | "zod": "^3.17.10" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /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-number.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | export const addTwoNumbers = (a, b) => { 4 | return a + b; 5 | }; 6 | 7 | it("Should add the two numbers together", () => { 8 | expect(addTwoNumbers(2, 4)).toEqual(6); 9 | expect(addTwoNumbers(10, 10)).toEqual(20); 10 | }); 11 | -------------------------------------------------------------------------------- /src/01-number.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | export const addTwoNumbers = (a: number, b: number) => { 4 | return a + b; 5 | }; 6 | 7 | it("Should add the two numbers together", () => { 8 | expect(addTwoNumbers(2, 4)).toEqual(6); 9 | expect(addTwoNumbers(10, 10)).toEqual(20); 10 | }); 11 | -------------------------------------------------------------------------------- /src/02-object-param.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | export const addTwoNumbers = (params) => { 4 | return params.first + params.second; 5 | }; 6 | 7 | it("Should add the two numbers together", () => { 8 | expect( 9 | addTwoNumbers({ 10 | first: 2, 11 | second: 4, 12 | }), 13 | ).toEqual(6); 14 | 15 | expect( 16 | addTwoNumbers({ 17 | first: 10, 18 | second: 20, 19 | }), 20 | ).toEqual(30); 21 | }); 22 | -------------------------------------------------------------------------------- /src/02-object-param.solution.1.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | export const addTwoNumbers = (params: { first: number; second: number }) => { 4 | return params.first + params.second; 5 | }; 6 | 7 | it("Should add the two numbers together", () => { 8 | expect( 9 | addTwoNumbers({ 10 | first: 2, 11 | second: 4, 12 | }), 13 | ).toEqual(6); 14 | 15 | expect( 16 | addTwoNumbers({ 17 | first: 10, 18 | second: 20, 19 | }), 20 | ).toEqual(30); 21 | }); 22 | -------------------------------------------------------------------------------- /src/02-object-param.solution.2.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | type AddTwoNumbersArgs = { 4 | first: number; 5 | second: number; 6 | }; 7 | 8 | export const addTwoNumbers = (params: AddTwoNumbersArgs) => { 9 | return params.first + params.second; 10 | }; 11 | 12 | it("Should add the two numbers together", () => { 13 | expect( 14 | addTwoNumbers({ 15 | first: 2, 16 | second: 4, 17 | }), 18 | ).toEqual(6); 19 | 20 | expect( 21 | addTwoNumbers({ 22 | first: 10, 23 | second: 20, 24 | }), 25 | ).toEqual(30); 26 | }); 27 | -------------------------------------------------------------------------------- /src/02-object-param.solution.3.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | interface AddTwoNumbersArgs { 4 | first: number; 5 | second: number; 6 | } 7 | 8 | export const addTwoNumbers = (params: AddTwoNumbersArgs) => { 9 | return params.first + params.second; 10 | }; 11 | 12 | it("Should add the two numbers together", () => { 13 | expect( 14 | addTwoNumbers({ 15 | first: 2, 16 | second: 4, 17 | }), 18 | ).toEqual(6); 19 | 20 | expect( 21 | addTwoNumbers({ 22 | first: 10, 23 | second: 20, 24 | }), 25 | ).toEqual(30); 26 | }); 27 | -------------------------------------------------------------------------------- /src/03-optional-properties.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | export const getName = (params: { first: string; last: string }) => { 4 | if (params.last) { 5 | return `${params.first} ${params.last}`; 6 | } 7 | return params.first; 8 | }; 9 | 10 | it("Should work with just the first name", () => { 11 | const name = getName({ 12 | first: "Matt", 13 | }); 14 | 15 | expect(name).toEqual("Matt"); 16 | }); 17 | 18 | it("Should work with the first and last name", () => { 19 | const name = getName({ 20 | first: "Matt", 21 | last: "Pocock", 22 | }); 23 | 24 | expect(name).toEqual("Matt Pocock"); 25 | }); 26 | -------------------------------------------------------------------------------- /src/03-optional-properties.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | export const getName = (params: { first: string; last?: string }) => { 4 | if (params.last) { 5 | return `${params.first} ${params.last}`; 6 | } 7 | return params.first; 8 | }; 9 | 10 | it("Should work with just the first name", () => { 11 | const name = getName({ 12 | first: "Matt", 13 | }); 14 | 15 | expect(name).toEqual("Matt"); 16 | }); 17 | 18 | it("Should work with the first and last name", () => { 19 | const name = getName({ 20 | first: "Matt", 21 | last: "Pocock", 22 | }); 23 | 24 | expect(name).toEqual("Matt Pocock"); 25 | }); 26 | -------------------------------------------------------------------------------- /src/04-optional-params.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | export const getName = (first: string, last: string) => { 4 | if (last) { 5 | return `${first} ${last}`; 6 | } 7 | return first; 8 | }; 9 | 10 | it("Should work with just the first name", () => { 11 | const name = getName("Matt"); 12 | 13 | expect(name).toEqual("Matt"); 14 | }); 15 | 16 | it("Should work with the first and last name", () => { 17 | const name = getName("Matt", "Pocock"); 18 | 19 | expect(name).toEqual("Matt Pocock"); 20 | }); 21 | -------------------------------------------------------------------------------- /src/04-optional-params.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | export const getName = (first: string, last?: string) => { 4 | if (last) { 5 | return `${first} ${last}`; 6 | } 7 | return first; 8 | }; 9 | 10 | it("Should work with just the first name", () => { 11 | const name = getName("Matt"); 12 | 13 | expect(name).toEqual("Matt"); 14 | }); 15 | 16 | it("Should work with the first and last name", () => { 17 | const name = getName("Matt", "Pocock"); 18 | 19 | expect(name).toEqual("Matt Pocock"); 20 | }); 21 | -------------------------------------------------------------------------------- /src/05-assigning-types-to-variables.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | interface User { 4 | id: number; 5 | firstName: string; 6 | lastName: string; 7 | isAdmin: boolean; 8 | } 9 | 10 | /** 11 | * How do we ensure that defaultUser is of type User 12 | * at THIS LINE - not further down in the code? 13 | */ 14 | const defaultUser = {}; 15 | 16 | const getUserId = (user: User) => { 17 | return user.id; 18 | }; 19 | 20 | it("Should get the user id", () => { 21 | expect(getUserId(defaultUser)).toEqual(1); 22 | }); 23 | -------------------------------------------------------------------------------- /src/05-assigning-types-to-variables.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | interface User { 4 | id: number; 5 | firstName: string; 6 | lastName: string; 7 | isAdmin: boolean; 8 | } 9 | 10 | /** 11 | * How do we ensure that defaultUser is of type User 12 | * at THIS LINE - not further down in the code? 13 | */ 14 | const defaultUser: User = { 15 | id: 1, 16 | firstName: "Matt", 17 | lastName: "Pocock", 18 | isAdmin: true, 19 | }; 20 | 21 | const getUserId = (user: User) => { 22 | return user.id; 23 | }; 24 | 25 | it("Should get the user id", () => { 26 | expect(getUserId(defaultUser)).toEqual(1); 27 | }); 28 | -------------------------------------------------------------------------------- /src/06-unions.problem.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | id: number; 3 | firstName: string; 4 | lastName: string; 5 | /** 6 | * How do we ensure that role is only one of: 7 | * - 'admin' 8 | * - 'user' 9 | * - 'super-admin' 10 | */ 11 | role: string; 12 | } 13 | 14 | export const defaultUser: User = { 15 | id: 1, 16 | firstName: "Matt", 17 | lastName: "Pocock", 18 | // @ts-expect-error 19 | role: "I_SHOULD_NOT_BE_ALLOWED", 20 | }; 21 | -------------------------------------------------------------------------------- /src/06-unions.solution.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | id: number; 3 | firstName: string; 4 | lastName: string; 5 | /** 6 | * How do we ensure that role is only one of: 7 | * - 'admin' 8 | * - 'user' 9 | * - 'super-admin' 10 | */ 11 | role: "admin" | "user" | "super-admin"; 12 | } 13 | 14 | export const defaultUser: User = { 15 | id: 1, 16 | firstName: "Matt", 17 | lastName: "Pocock", 18 | // @ts-expect-error 19 | role: "I_SHOULD_NOT_BE_ALLOWED", 20 | }; 21 | -------------------------------------------------------------------------------- /src/07-arrays.problem.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | id: number; 3 | firstName: string; 4 | lastName: string; 5 | role: "admin" | "user" | "super-admin"; 6 | posts: Post; 7 | } 8 | 9 | interface Post { 10 | id: number; 11 | title: string; 12 | } 13 | 14 | export const defaultUser: User = { 15 | id: 1, 16 | firstName: "Matt", 17 | lastName: "Pocock", 18 | role: "admin", 19 | posts: [ 20 | { 21 | id: 1, 22 | title: "How I eat so much cheese", 23 | }, 24 | { 25 | id: 2, 26 | title: "Why I don't eat more vegetables", 27 | }, 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /src/07-arrays.solution.1.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | id: number; 3 | firstName: string; 4 | lastName: string; 5 | role: "admin" | "user" | "super-admin"; 6 | posts: Post[]; 7 | } 8 | 9 | interface Post { 10 | id: number; 11 | title: string; 12 | } 13 | 14 | export const defaultUser: User = { 15 | id: 1, 16 | firstName: "Matt", 17 | lastName: "Pocock", 18 | role: "admin", 19 | posts: [ 20 | { 21 | id: 1, 22 | title: "How I eat so much cheese", 23 | }, 24 | { 25 | id: 2, 26 | title: "Why I don't eat more vegetables", 27 | }, 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /src/07-arrays.solution.2.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | id: number; 3 | firstName: string; 4 | lastName: string; 5 | role: "admin" | "user" | "super-admin"; 6 | posts: Array; 7 | } 8 | 9 | interface Post { 10 | id: number; 11 | title: string; 12 | } 13 | 14 | export const defaultUser: User = { 15 | id: 1, 16 | firstName: "Matt", 17 | lastName: "Pocock", 18 | role: "admin", 19 | posts: [ 20 | { 21 | id: 1, 22 | title: "How I eat so much cheese", 23 | }, 24 | { 25 | id: 2, 26 | title: "Why I don't eat more vegetables", 27 | }, 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /src/08-function-return-type-annotations.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | interface User { 4 | id: number; 5 | firstName: string; 6 | lastName: string; 7 | role: "admin" | "user" | "super-admin"; 8 | posts: Array; 9 | } 10 | 11 | interface Post { 12 | id: number; 13 | title: string; 14 | } 15 | 16 | /** 17 | * How do we ensure that makeUser ALWAYS 18 | * returns a user? 19 | */ 20 | const makeUser = () => { 21 | return {}; 22 | }; 23 | 24 | it("Should return a valid user", () => { 25 | const user = makeUser(); 26 | 27 | expect(user.id).toBeTypeOf("number"); 28 | expect(user.firstName).toBeTypeOf("string"); 29 | expect(user.lastName).toBeTypeOf("string"); 30 | expect(user.role).to.be.oneOf(["super-admin", "admin", "user"]); 31 | 32 | expect(user.posts[0].id).toBeTypeOf("number"); 33 | expect(user.posts[0].title).toBeTypeOf("string"); 34 | }); 35 | -------------------------------------------------------------------------------- /src/08-function-return-type-annotations.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | interface User { 4 | id: number; 5 | firstName: string; 6 | lastName: string; 7 | role: "admin" | "user" | "super-admin"; 8 | posts: Array; 9 | } 10 | 11 | interface Post { 12 | id: number; 13 | title: string; 14 | } 15 | 16 | /** 17 | * How do we ensure that makeUser ALWAYS 18 | * returns a user? 19 | */ 20 | const makeUser = (): User => { 21 | return { 22 | id: 1, 23 | firstName: "Matt", 24 | lastName: "Pocock", 25 | role: "admin", 26 | posts: [ 27 | { 28 | id: 1, 29 | title: "How I eat so much cheese", 30 | }, 31 | ], 32 | }; 33 | }; 34 | 35 | it("Should return a valid user", () => { 36 | const user = makeUser(); 37 | 38 | expect(user.id).toBeTypeOf("number"); 39 | expect(user.firstName).toBeTypeOf("string"); 40 | expect(user.lastName).toBeTypeOf("string"); 41 | expect(user.role).to.be.oneOf(["super-admin", "admin", "user"]); 42 | 43 | expect(user.posts[0].id).toBeTypeOf("number"); 44 | expect(user.posts[0].title).toBeTypeOf("string"); 45 | }); 46 | -------------------------------------------------------------------------------- /src/09-promises.problem.ts: -------------------------------------------------------------------------------- 1 | interface LukeSkywalker { 2 | name: string; 3 | height: string; 4 | mass: string; 5 | hair_color: string; 6 | skin_color: string; 7 | eye_color: string; 8 | birth_year: string; 9 | gender: string; 10 | } 11 | 12 | export const fetchLukeSkywalker = async (): LukeSkywalker => { 13 | const data = await fetch("https://swapi.py4e.com/api/people/1").then( 14 | (res) => { 15 | return res.json(); 16 | } 17 | ); 18 | 19 | return data; 20 | }; 21 | -------------------------------------------------------------------------------- /src/09-promises.solution.1.ts: -------------------------------------------------------------------------------- 1 | interface LukeSkywalker { 2 | name: string; 3 | height: string; 4 | mass: string; 5 | hair_color: string; 6 | skin_color: string; 7 | eye_color: string; 8 | birth_year: string; 9 | gender: string; 10 | } 11 | 12 | export const fetchLukeSkywalker = async (): Promise => { 13 | const data = await fetch("https://swapi.py4e.com/api/people/1").then( 14 | (res) => { 15 | return res.json(); 16 | } 17 | ); 18 | 19 | return data; 20 | }; 21 | -------------------------------------------------------------------------------- /src/09-promises.solution.2.ts: -------------------------------------------------------------------------------- 1 | interface LukeSkywalker { 2 | name: string; 3 | height: string; 4 | mass: string; 5 | hair_color: string; 6 | skin_color: string; 7 | eye_color: string; 8 | birth_year: string; 9 | gender: string; 10 | } 11 | 12 | export const fetchLukeSkywalker = async () => { 13 | const data = await fetch("https://swapi.py4e.com/api/people/1").then( 14 | (res) => { 15 | return res.json(); 16 | } 17 | ); 18 | 19 | return data as LukeSkywalker; 20 | }; 21 | -------------------------------------------------------------------------------- /src/09-promises.solution.3.ts: -------------------------------------------------------------------------------- 1 | interface LukeSkywalker { 2 | name: string; 3 | height: string; 4 | mass: string; 5 | hair_color: string; 6 | skin_color: string; 7 | eye_color: string; 8 | birth_year: string; 9 | gender: string; 10 | } 11 | 12 | export const fetchLukeSkywalker = async () => { 13 | const data: LukeSkywalker = await fetch( 14 | "https://swapi.py4e.com/api/people/1" 15 | ).then((res) => { 16 | return res.json(); 17 | }); 18 | 19 | return data; 20 | }; 21 | -------------------------------------------------------------------------------- /src/10-set.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "./helpers/type-utils"; 3 | 4 | const guitarists = new Set(); 5 | 6 | guitarists.add("Jimi Hendrix"); 7 | guitarists.add("Eric Clapton"); 8 | 9 | it("Should contain Jimi and Eric", () => { 10 | expect(guitarists.has("Jimi Hendrix")).toEqual(true); 11 | expect(guitarists.has("Eric Clapton")).toEqual(true); 12 | }); 13 | 14 | it("Should give a type error when you try to pass a non-string", () => { 15 | // @ts-expect-error 16 | guitarists.add(2); 17 | }); 18 | 19 | it("Should be typed as an array of strings", () => { 20 | const guitaristsAsArray = Array.from(guitarists); 21 | 22 | type tests = [Expect>]; 23 | }); 24 | -------------------------------------------------------------------------------- /src/10-set.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { Equal, Expect } from "./helpers/type-utils"; 3 | 4 | const guitarists = new Set(); 5 | 6 | guitarists.add("Jimi Hendrix"); 7 | guitarists.add("Eric Clapton"); 8 | 9 | it("Should contain Jimi and Eric", () => { 10 | expect(guitarists.has("Jimi Hendrix")).toEqual(true); 11 | expect(guitarists.has("Eric Clapton")).toEqual(true); 12 | }); 13 | 14 | it("Should give a type error when you try to pass a non-string", () => { 15 | // @ts-expect-error 16 | guitarists.add(2); 17 | }); 18 | 19 | it("Should be typed as an array of strings", () => { 20 | const guitaristsAsArray = Array.from(guitarists); 21 | 22 | type tests = [Expect>]; 23 | }); 24 | -------------------------------------------------------------------------------- /src/11-record.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | const createCache = () => { 4 | const cache = {}; 5 | 6 | const add = (id: string, value: string) => { 7 | cache[id] = value; 8 | }; 9 | 10 | const remove = (id: string) => { 11 | delete cache[id]; 12 | }; 13 | 14 | return { 15 | cache, 16 | add, 17 | remove, 18 | }; 19 | }; 20 | 21 | it("Should add values to the cache", () => { 22 | const cache = createCache(); 23 | 24 | cache.add("123", "Matt"); 25 | 26 | expect(cache.cache["123"]).toEqual("Matt"); 27 | }); 28 | 29 | it("Should remove values from the cache", () => { 30 | const cache = createCache(); 31 | 32 | cache.add("123", "Matt"); 33 | cache.remove("123"); 34 | 35 | expect(cache.cache["123"]).toEqual(undefined); 36 | }); 37 | -------------------------------------------------------------------------------- /src/11-record.solution.1.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | const createCache = () => { 4 | const cache: Record = {}; 5 | 6 | const add = (id: string, value: string) => { 7 | cache[id] = value; 8 | }; 9 | 10 | const remove = (id: string) => { 11 | delete cache[id]; 12 | }; 13 | 14 | return { 15 | cache, 16 | add, 17 | remove, 18 | }; 19 | }; 20 | 21 | it("Should add values to the cache", () => { 22 | const cache = createCache(); 23 | 24 | cache.add("123", "Matt"); 25 | 26 | expect(cache.cache["123"]).toEqual("Matt"); 27 | }); 28 | 29 | it("Should remove values from the cache", () => { 30 | const cache = createCache(); 31 | 32 | cache.add("123", "Matt"); 33 | cache.remove("123"); 34 | 35 | expect(cache.cache["123"]).toEqual(undefined); 36 | }); 37 | -------------------------------------------------------------------------------- /src/11-record.solution.2.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | const createCache = () => { 4 | const cache: { 5 | [id: string]: string; 6 | } = {}; 7 | 8 | const add = (id: string, value: string) => { 9 | cache[id] = value; 10 | }; 11 | 12 | const remove = (id: string) => { 13 | delete cache[id]; 14 | }; 15 | 16 | return { 17 | cache, 18 | add, 19 | remove, 20 | }; 21 | }; 22 | 23 | it("Should add values to the cache", () => { 24 | const cache = createCache(); 25 | 26 | cache.add("123", "Matt"); 27 | 28 | expect(cache.cache["123"]).toEqual("Matt"); 29 | }); 30 | 31 | it("Should remove values from the cache", () => { 32 | const cache = createCache(); 33 | 34 | cache.add("123", "Matt"); 35 | cache.remove("123"); 36 | 37 | expect(cache.cache["123"]).toEqual(undefined); 38 | }); 39 | -------------------------------------------------------------------------------- /src/11-record.solution.3.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | interface Cache { 4 | [id: string]: string; 5 | } 6 | 7 | const createCache = () => { 8 | const cache: Cache = {}; 9 | 10 | const add = (id: string, value: string) => { 11 | cache[id] = value; 12 | }; 13 | 14 | const remove = (id: string) => { 15 | delete cache[id]; 16 | }; 17 | 18 | return { 19 | cache, 20 | add, 21 | remove, 22 | }; 23 | }; 24 | 25 | it("Should add values to the cache", () => { 26 | const cache = createCache(); 27 | 28 | cache.add("123", "Matt"); 29 | 30 | expect(cache.cache["123"]).toEqual("Matt"); 31 | }); 32 | 33 | it("Should remove values from the cache", () => { 34 | const cache = createCache(); 35 | 36 | cache.add("123", "Matt"); 37 | cache.remove("123"); 38 | 39 | expect(cache.cache["123"]).toEqual(undefined); 40 | }); 41 | -------------------------------------------------------------------------------- /src/12-typeof-narrowing.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | const coerceAmount = (amount: number | { amount: number }) => {}; 4 | 5 | it("Should return the amount when passed an object", () => { 6 | expect(coerceAmount({ amount: 20 })).toEqual(20); 7 | }); 8 | 9 | it("Should return the amount when passed a number", () => { 10 | expect(coerceAmount(20)).toEqual(20); 11 | }); 12 | -------------------------------------------------------------------------------- /src/12-typeof-narrowing.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | const coerceAmount = (amount: number | { amount: number }) => { 4 | if (typeof amount === "number") { 5 | return amount; 6 | } 7 | return amount.amount; 8 | }; 9 | 10 | it("Should return the amount when passed an object", () => { 11 | expect(coerceAmount({ amount: 20 })).toEqual(20); 12 | }); 13 | 14 | it("Should return the amount when passed a number", () => { 15 | expect(coerceAmount(20)).toEqual(20); 16 | }); 17 | -------------------------------------------------------------------------------- /src/13-catch-blocks.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | const tryCatchDemo = (state: "fail" | "succeed") => { 4 | try { 5 | if (state === "fail") { 6 | throw new Error("Failure!"); 7 | } 8 | } catch (e) { 9 | return e.message; 10 | } 11 | }; 12 | 13 | it("Should return the message when it fails", () => { 14 | expect(tryCatchDemo("fail")).toEqual("Failure!"); 15 | }); 16 | -------------------------------------------------------------------------------- /src/13-catch-blocks.solution.1.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | const tryCatchDemo = (state: "fail" | "succeed") => { 4 | try { 5 | if (state === "fail") { 6 | throw new Error("Failure!"); 7 | } 8 | } catch (e: any) { 9 | return e.message; 10 | } 11 | }; 12 | 13 | it("Should return the message when it fails", () => { 14 | expect(tryCatchDemo("fail")).toEqual("Failure!"); 15 | }); 16 | -------------------------------------------------------------------------------- /src/13-catch-blocks.solution.2.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | const tryCatchDemo = (state: "fail" | "succeed") => { 4 | try { 5 | if (state === "fail") { 6 | throw new Error("Failure!"); 7 | } 8 | } catch (e) { 9 | return (e as Error).message; 10 | } 11 | }; 12 | 13 | it("Should return the message when it fails", () => { 14 | expect(tryCatchDemo("fail")).toEqual("Failure!"); 15 | }); 16 | -------------------------------------------------------------------------------- /src/13-catch-blocks.solution.3.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | const tryCatchDemo = (state: "fail" | "succeed") => { 4 | try { 5 | if (state === "fail") { 6 | throw new Error("Failure!"); 7 | } 8 | } catch (e) { 9 | if (e instanceof Error) { 10 | return e.message; 11 | } 12 | } 13 | }; 14 | 15 | it("Should return the message when it fails", () => { 16 | expect(tryCatchDemo("fail")).toEqual("Failure!"); 17 | }); 18 | -------------------------------------------------------------------------------- /src/14-extends.problem.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "./helpers/type-utils"; 2 | 3 | /** 4 | * Here, the id property is shared between all three 5 | * interfaces. Can you find a way to refactor this to 6 | * make it more DRY? 7 | */ 8 | 9 | interface User { 10 | id: string; 11 | firstName: string; 12 | lastName: string; 13 | } 14 | 15 | interface Post { 16 | id: string; 17 | title: string; 18 | body: string; 19 | } 20 | 21 | interface Comment { 22 | id: string; 23 | comment: string; 24 | } 25 | 26 | type tests = [ 27 | Expect>, 28 | Expect>, 29 | Expect>, 30 | ]; 31 | -------------------------------------------------------------------------------- /src/14-extends.solution.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "./helpers/type-utils"; 2 | 3 | /** 4 | * Here, the id property is shared between all three 5 | * interfaces. Can you find a way to refactor this to 6 | * make it more DRY? 7 | */ 8 | 9 | interface Base { 10 | id: string; 11 | } 12 | 13 | interface User extends Base { 14 | firstName: string; 15 | lastName: string; 16 | } 17 | 18 | interface Post extends Base { 19 | title: string; 20 | body: string; 21 | } 22 | 23 | interface Comment extends Base { 24 | comment: string; 25 | } 26 | 27 | type tests = [ 28 | Expect>, 29 | Expect>, 30 | Expect>, 31 | ]; 32 | -------------------------------------------------------------------------------- /src/15-intersection.problem.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | id: string; 3 | firstName: string; 4 | lastName: string; 5 | } 6 | 7 | interface Post { 8 | id: string; 9 | title: string; 10 | body: string; 11 | } 12 | 13 | /** 14 | * How do we type this return statement so it's both 15 | * User AND { posts: Post[] } 16 | */ 17 | export const getDefaultUserAndPosts = (): unknown => { 18 | return { 19 | id: "1", 20 | firstName: "Matt", 21 | lastName: "Pocock", 22 | posts: [ 23 | { 24 | id: "1", 25 | title: "How I eat so much cheese", 26 | body: "It's pretty edam difficult", 27 | }, 28 | ], 29 | }; 30 | }; 31 | 32 | const userAndPosts = getDefaultUserAndPosts(); 33 | 34 | console.log(userAndPosts.posts[0]); 35 | -------------------------------------------------------------------------------- /src/15-intersection.solution.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | id: string; 3 | firstName: string; 4 | lastName: string; 5 | } 6 | 7 | interface Post { 8 | id: string; 9 | title: string; 10 | body: string; 11 | } 12 | 13 | export const getDefaultUserAndPosts = (): User & { posts: Post[] } => { 14 | return { 15 | id: "1", 16 | firstName: "Matt", 17 | lastName: "Pocock", 18 | posts: [ 19 | { 20 | id: "1", 21 | title: "How I eat so much cheese", 22 | body: "It's pretty edam difficult", 23 | }, 24 | ], 25 | }; 26 | }; 27 | 28 | const userAndPosts = getDefaultUserAndPosts(); 29 | 30 | console.log(userAndPosts.posts[0]); 31 | -------------------------------------------------------------------------------- /src/16-omit-and-pick.problem.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "./helpers/type-utils"; 2 | 3 | interface User { 4 | id: string; 5 | firstName: string; 6 | lastName: string; 7 | } 8 | 9 | /** 10 | * How do we create a new object type with _only_ the 11 | * firstName and lastName properties of User? 12 | */ 13 | 14 | type MyType = unknown; 15 | 16 | type tests = [Expect>]; 17 | -------------------------------------------------------------------------------- /src/16-omit-and-pick.solution.1.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "./helpers/type-utils"; 2 | 3 | interface User { 4 | id: string; 5 | firstName: string; 6 | lastName: string; 7 | } 8 | 9 | /** 10 | * How do we create a new object type with _only_ the 11 | * firstName and lastName properties of User? 12 | */ 13 | 14 | type MyType = Omit; 15 | 16 | type tests = [Expect>]; 17 | -------------------------------------------------------------------------------- /src/16-omit-and-pick.solution.2.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "./helpers/type-utils"; 2 | 3 | interface User { 4 | id: string; 5 | firstName: string; 6 | lastName: string; 7 | } 8 | 9 | /** 10 | * How do we create a new object type with _only_ the 11 | * firstName and lastName properties of User? 12 | */ 13 | 14 | type MyType = Pick; 15 | 16 | type tests = [Expect>]; 17 | -------------------------------------------------------------------------------- /src/17-function-types.problem.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "./helpers/type-utils"; 2 | 3 | /** 4 | * How do we type onFocusChange? 5 | */ 6 | const addListener = (onFocusChange: unknown) => { 7 | window.addEventListener("focus", () => { 8 | onFocusChange(true); 9 | }); 10 | 11 | window.addEventListener("blur", () => { 12 | onFocusChange(false); 13 | }); 14 | }; 15 | 16 | addListener((isFocused) => { 17 | console.log({ isFocused }); 18 | 19 | type tests = [Expect>]; 20 | }); 21 | -------------------------------------------------------------------------------- /src/17-function-types.solution.1.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "./helpers/type-utils"; 2 | 3 | /** 4 | * How do we type onFocusChange? 5 | */ 6 | const addListener = (onFocusChange: (isFocused: boolean) => void) => { 7 | window.addEventListener("focus", () => { 8 | onFocusChange(true); 9 | }); 10 | 11 | window.addEventListener("blur", () => { 12 | onFocusChange(false); 13 | }); 14 | }; 15 | 16 | addListener((isFocused) => { 17 | console.log({ isFocused }); 18 | 19 | type tests = [Expect>]; 20 | }); 21 | -------------------------------------------------------------------------------- /src/17-function-types.solution.2.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect } from "./helpers/type-utils"; 2 | 3 | /** 4 | * How do we type onFocusChange? 5 | */ 6 | type FocusListener = (isFocused: boolean) => void; 7 | 8 | const addListener = (onFocusChange: FocusListener) => { 9 | window.addEventListener("focus", () => { 10 | onFocusChange(true); 11 | }); 12 | 13 | window.addEventListener("blur", () => { 14 | onFocusChange(false); 15 | }); 16 | }; 17 | 18 | addListener((isFocused) => { 19 | console.log({ isFocused }); 20 | 21 | type tests = [Expect>]; 22 | }); 23 | -------------------------------------------------------------------------------- /src/18-function-types-with-promises.problem.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | interface User { 4 | id: string; 5 | firstName: string; 6 | lastName: string; 7 | } 8 | 9 | const createThenGetUser = async ( 10 | createUser: unknown, 11 | getUser: unknown, 12 | ): Promise => { 13 | const userId: string = await createUser(); 14 | 15 | const user = await getUser(userId); 16 | 17 | return user; 18 | }; 19 | 20 | it("Should create the user, then get them", async () => { 21 | const user = await createThenGetUser( 22 | async () => "123", 23 | async (id) => ({ 24 | id, 25 | firstName: "Matt", 26 | lastName: "Pocock", 27 | }), 28 | ); 29 | 30 | expect(user).toEqual({ 31 | id: "123", 32 | firstName: "Matt", 33 | lastName: "Pocock", 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/18-function-types-with-promises.solution.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | interface User { 4 | id: string; 5 | firstName: string; 6 | lastName: string; 7 | } 8 | 9 | const createThenGetUser = async ( 10 | createUser: () => Promise, 11 | getUser: (id: string) => Promise, 12 | ): Promise => { 13 | const userId: string = await createUser(); 14 | 15 | const user = await getUser(userId); 16 | 17 | return user; 18 | }; 19 | 20 | it("Should create the user, then get them", async () => { 21 | const user = await createThenGetUser( 22 | async () => "123", 23 | async (id) => ({ 24 | id, 25 | firstName: "Matt", 26 | lastName: "Pocock", 27 | }), 28 | ); 29 | 30 | expect(user).toEqual({ 31 | id: "123", 32 | firstName: "Matt", 33 | lastName: "Pocock", 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/helpers/type-utils.ts: -------------------------------------------------------------------------------- 1 | export type Expect = T; 2 | export type ExpectTrue = T; 3 | export type ExpectFalse = T; 4 | export type IsTrue = T; 5 | export type IsFalse = T; 6 | 7 | export type Equal = (() => T extends X ? 1 : 2) extends < 8 | T, 9 | >() => T extends Y ? 1 : 2 10 | ? true 11 | : false; 12 | export type NotEqual = true extends Equal ? false : true; 13 | 14 | // https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360 15 | export type IsAny = 0 extends 1 & T ? true : false; 16 | export type NotAny = true extends IsAny ? false : true; 17 | 18 | export type Debug = { [K in keyof T]: T[K] }; 19 | export type MergeInsertions = T extends object 20 | ? { [K in keyof T]: MergeInsertions } 21 | : T; 22 | 23 | export type Alike = Equal, MergeInsertions>; 24 | 25 | export type ExpectExtends = EXPECTED extends VALUE 26 | ? true 27 | : false; 28 | export type ExpectValidArgs< 29 | FUNC extends (...args: any[]) => any, 30 | ARGS extends any[], 31 | > = ARGS extends Parameters ? true : false; 32 | 33 | export type UnionToIntersection = ( 34 | U extends any ? (k: U) => void : never 35 | ) extends (k: infer I) => void 36 | ? I 37 | : never; 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@total-typescript/tsconfig/bundler", 3 | "compilerOptions": { 4 | "noUncheckedIndexedAccess": false, 5 | "verbatimModuleSyntax": false 6 | }, 7 | "include": [ 8 | "src" 9 | ] 10 | } -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------