Skip to content

Commit 120eb18

Browse files
committed
Completed workshop setup
0 parents  commit 120eb18

29 files changed

+3183
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Advanced TypeScript Workshop
2+
3+
Hello! My name's [Matt Pocock](https://twitter.com/mattpocockuk). This is a workshop repo to teach you about Advanced TypeScript.
4+
5+
## Topics Covered
6+
7+
- Using `typeof` and `as const` to derive types from runtime
8+
- Generics
9+
- Assertion functions & type predicates
10+
- Template literal types
11+
- Conditional types & `infer`
12+
- Diagnosing type errors
13+
14+
The plan of the workshop is to get you more confident with complex type signatures, less likely to want to use `any`, and faster at diagnosing errors.
15+
16+
## Workshop Plan
17+
18+
We'll be running from 9AM PT - 2PM PT. Here's the plan:
19+
20+
09:00-9:15: Setup & Housekeeping
21+
09:15-10:30: First session
22+
10:30-10:45: Coffee Break
23+
10:45-12:00: Second session
24+
12:00-12:30: Lunch
25+
12:30-14:00: Third session
26+
27+
We'll be working through the material in this repository, mostly in small groups.
28+
29+
## System Requirements
30+
31+
- git v2.14.1 or greater
32+
- NodeJS v16.15.0 or greater
33+
- npm v8.5.0 or greater
34+
35+
All of these must be available in your `PATH`. To verify things are set up properly, you can run this:
36+
37+
```bash
38+
git --version
39+
node --version
40+
npm --version
41+
```
42+
43+
## Setup
44+
45+
After you've made sure to have the correct things (and versions) installed, you should be able to just run a few commands to get set up:
46+
47+
```bash
48+
git clone https://github.com/mattpocock/advanced-typescript-workshop.git
49+
cd advanced-typescript-workshop
50+
npm install
51+
```
52+
53+
That's it! You'll now have all the dependencies you need to work through the workshop exercises.
54+
55+
## Exercises
56+
57+
Exercises are in the [`./exercises`](./exercises) folder. They're designed to be worked through one after the other.
58+
59+
Each exercise follows a similar pattern:
60+
61+
- Look at the file with the `*.code.ts` extension. This gives you the code we're going to be working through and trying to understand.
62+
- Read through the `*.exercise.ts` file. Read through the file, comment-by-comment, and follow the instructions by editing the file inline.
63+
- Wherever you see reference to `Solution #1`, check the `*.solutions.ts` file if you need to check the solution.
64+
65+
### Emoji
66+
67+
Exercises use emoji to express various different things:
68+
69+
- 💡 - A new idea appears!
70+
- 🛠 - Write some code!
71+
- 🧑‍💻 - Your team lead has some thoughts...
72+
- 🕵️‍♂️ - Time for an investigation...
73+
- ⛔️ - Eek! A type error.
74+
- ✅ - Hooray! The type error was fixed.
75+
- 🚁 - Hover over something.
76+
- 🔮 - Do a go-to-definition.

exercises/01-apiMapping.code.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export const programModeEnumMap = {
2+
GROUP: "group",
3+
ANNOUNCEMENT: "announcement",
4+
ONE_ON_ONE: "1on1",
5+
SELF_DIRECTED: "selfDirected",
6+
PLANNED_ONE_ON_ONE: "planned1on1",
7+
PLANNED_SELF_DIRECTED: "plannedSelfDirected",
8+
} as const;
9+
10+
export type ProgramMap = typeof programModeEnumMap;
11+
12+
export type Program = ProgramMap[keyof ProgramMap];
13+
export type IndividualProgram = ProgramMap[
14+
| "ONE_ON_ONE"
15+
| "SELF_DIRECTED"
16+
| "PLANNED_ONE_ON_ONE"
17+
| "PLANNED_SELF_DIRECTED"];
18+
19+
export type GroupProgram = ProgramMap["GROUP"];

exercises/01-apiMapping.exercise.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/**
2+
* 💡 In this example, our backend is using a slightly
3+
* different enum to us. So we've build a map to be able
4+
* to easily access the backend enum (in SCREAMING_SNAKE_CASE)
5+
* to frontend (in camelCase)
6+
*/
7+
8+
export const programModeEnumMap = {
9+
// ^ 🚁
10+
GROUP: "group",
11+
ANNOUNCEMENT: "announcement",
12+
ONE_ON_ONE: "1on1",
13+
SELF_DIRECTED: "selfDirected",
14+
PLANNED_ONE_ON_ONE: "planned1on1",
15+
PLANNED_SELF_DIRECTED: "plannedSelfDirected",
16+
} as const;
17+
18+
/**
19+
* 🚁 Hover over programModeEnumMap. It should display
20+
* as an object with LOTS of readonly properties, all inferred
21+
* as their literal type ("group", "announcement" etc).
22+
*/
23+
24+
export type ProgramMap = typeof programModeEnumMap;
25+
/** ^ 🚁
26+
*
27+
* 🚁 Hover over ProgramMap - it should be exactly the same
28+
* display as when you hovered over programModeEnumMap
29+
*/
30+
31+
export type Program = ProgramMap[keyof ProgramMap];
32+
/** ^ 🚁
33+
*
34+
* 🚁 Program is being inferred as a union type of all of
35+
* the frontend enums. Interesting.
36+
*/
37+
38+
// ⬇️ 🚁
39+
export type IndividualProgram = ProgramMap[
40+
| "ONE_ON_ONE"
41+
| "SELF_DIRECTED"
42+
| "PLANNED_ONE_ON_ONE"
43+
| "PLANNED_SELF_DIRECTED"];
44+
45+
/**
46+
* 🚁 IndividualProgram is all of the enums, EXCEPT for
47+
* the ones with the keys of GROUP and ANNOUNCEMENT
48+
*/
49+
50+
export type GroupProgram = ProgramMap["GROUP"];
51+
/** ^ 🚁
52+
*
53+
* 🚁 GroupProgram is just the "group" member of the union.
54+
*/
55+
56+
/**
57+
* 🛠 OK, we know what each section does. Now let's recreate
58+
* it. Comment out all of the code EXCEPT for the
59+
* programModeEnumMap.
60+
*/
61+
62+
/**
63+
* 💡 This code is just JavaScript - except for a little extra
64+
* annotation: "as const"
65+
*
66+
* https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions
67+
*
68+
* Let's investigate what this does.
69+
*/
70+
71+
/**
72+
* 🛠 Remove the "as const" annotation:
73+
*
74+
* export const programModeEnumMap = {
75+
* GROUP: "group",
76+
* ANNOUNCEMENT: "announcement",
77+
* ONE_ON_ONE: "1on1",
78+
* SELF_DIRECTED: "selfDirected",
79+
* PLANNED_ONE_ON_ONE: "planned1on1",
80+
* PLANNED_SELF_DIRECTED: "plannedSelfDirected",
81+
* };
82+
*
83+
* 🚁 Now, hover over programModeEnumMap. You'll
84+
* notice that each of the properties are now inferred
85+
* as string, not their literal types.
86+
*
87+
* Why is this? It's because objects in JavaScript are
88+
* mutable by default.
89+
*
90+
* 🕵️‍♂️ Try reassigning one of the properties of
91+
* programModeEnumMap:
92+
*
93+
* programModeEnumMap.GROUP = 'some-other-thing';
94+
*
95+
* You'll see that it doesn't throw an error.
96+
*/
97+
98+
/**
99+
* 🛠 Add the as const annotation back in.
100+
*
101+
* ⛔️ You'll see that the line we wrote above is now erroring!
102+
*
103+
* Cannot assign to 'GROUP' because it is a read-only property.
104+
*
105+
* 🛠 Remove that line of code to clear the error.
106+
*
107+
* 💡 This is good! We don't want our config objects to be mutable.
108+
* You can also achieve this with Object.freeze, but this only
109+
* works one level deep on objects. 'as const' works recursively
110+
* down the entire object.
111+
*/
112+
113+
/**
114+
* 💡 We now want to derive our enum type from this object
115+
* so that we can use it in the rest of our application.
116+
*
117+
* To do that, we're going to need to pull it from the
118+
* runtime world into the type world.
119+
*
120+
* We'll need to use typeof for that:
121+
*
122+
* https://www.typescriptlang.org/docs/handbook/2/typeof-types.html
123+
*
124+
* 🛠 Declare a new type called ProgramMap, which uses typeof
125+
* on the programModeEnumMap:
126+
*
127+
* type ProgramMap = typeof programModeEnumMap;
128+
*/
129+
130+
/**
131+
* 💡 All this does is pull the inferred type of programModeEnumMap
132+
* into the type world, so we can manipulate it using TS syntax.
133+
*
134+
* 🛠 Let's declare a new type, GroupProgram, and make it the property
135+
* 'GROUP' of ProgramMap:
136+
*
137+
* type GroupProgram = ProgramMap["GROUP"];
138+
* ^ 🚁
139+
*
140+
* 🚁 Hover over GroupProgram - it should be inferred as "group"
141+
*/
142+
143+
/**
144+
* 💡 "as const" and typeof are a powerful combination, because
145+
* they let us do really clever things with config objects.
146+
*
147+
* Without "as const", this inference would look very different.
148+
*
149+
* 🕵️‍♂️ Try removing "as const" again. When hovering over GroupProgram,
150+
* it will now be inferred as a string! So "as const" is crucial
151+
* to deriving types from config objects. Add it back in again.
152+
*/
153+
154+
/**
155+
* 💡 Let's now recreate the IndividualProgram type, which represents
156+
* all the members of the enum that are for individuals only.
157+
*
158+
* We want to create a union type of all the possible individual
159+
* programs. To do that, you can pass in a union type to the property
160+
* access!
161+
*
162+
* 🛠 Create a new type called IndividualProgram. Make it a property
163+
* access on ProgramMap, but pass in ONE_ON_ONE, SELF_DIRECTED,
164+
* PLANNED_ONE_ON_ONE and PLANNED_SELF_DIRECTED as a union.
165+
*
166+
* ⬇️ 🚁
167+
* export type IndividualProgram = ProgramMap[
168+
* | "ONE_ON_ONE"
169+
* | "SELF_DIRECTED"
170+
* | "PLANNED_ONE_ON_ONE"
171+
* | "PLANNED_SELF_DIRECTED"];
172+
*
173+
* 🚁 Hover over IndividualProgram - it should be a union of
174+
* "1on1" | "selfDirected" | "planned1on1" | "plannedSelfDirected"
175+
*
176+
* 🕵️‍♂️ Try altering some of the members of the union you're passing in,
177+
* noticing how IndividualProgram also gets altered.
178+
*/
179+
180+
/**
181+
* 💡 This concept, that you can access objects via a union type to
182+
* RETURN a union type, is critical for understanding complex types.
183+
*
184+
* 💡 We still don't have a union type for ALL of the members of
185+
* the enum. To do that, we'd need to pass a union of ALL the keys
186+
* of ProgramMap BACK to ProgramMap.
187+
*
188+
* For that, we're going to use keyof:
189+
*
190+
* https://www.typescriptlang.org/docs/handbook/2/keyof-types.html
191+
*/
192+
193+
/**
194+
* 🛠 Let's create a type called BackendProgram. Assign it to
195+
* keyof ProgramMap:
196+
*
197+
* type BackendProgram = keyof ProgramMap;
198+
* ^ 🚁
199+
*
200+
* 🚁 Hover over BackendProgram. You should see that it's a union
201+
* of all of the keys in programModeEnumMap.
202+
*/
203+
204+
/**
205+
* 🛠 Create a new type called Program, which accesses ProgramMap with
206+
* ALL of ProgramMap's keys:
207+
*
208+
* type Program = ProgramMap[BackendProgram];
209+
* ^ 🚁
210+
*
211+
* 🚁 You'll see that Program is typed as a union of all of the
212+
* members of the frontend enum:
213+
*
214+
* "group" | "announcement" | "1on1" | "selfDirected"
215+
* | "planned1on1" | "plannedSelfDirected"
216+
*/
217+
218+
/**
219+
* 💡 You can use this pattern: Obj[keyof Obj] as a kind of
220+
* Object.values for the type world.
221+
*/
222+
223+
/**
224+
* 💡 Well done! We've covered 'as const', 'keyof', and accessing
225+
* index types via unions.
226+
*
227+
* 🕵️‍♂️ Try experimenting with programModeEnumMap, changing the keys
228+
* and values to see what errors, or what changes in the each of
229+
* the derived types.
230+
*/

exercises/02-roleBasedAccess.code.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const userAccessModel = {
2+
user: ["update-self", "view"],
3+
admin: ["create", "update-self", "update-any", "delete", "view"],
4+
anonymous: ["view"],
5+
} as const;
6+
7+
export type Role = keyof typeof userAccessModel;
8+
export type Action = typeof userAccessModel[Role][number];
9+
10+
export const canUserAccess = (role: Role, action: Action) => {
11+
return (userAccessModel[role] as ReadonlyArray<Action>).includes(action);
12+
};

0 commit comments

Comments
 (0)