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