-
Notifications
You must be signed in to change notification settings - Fork 134
/
02-roleBasedAccess.exercise.ts
204 lines (192 loc) ยท 6.23 KB
/
02-roleBasedAccess.exercise.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
/**
* ๐งโ๐ป Here, we've got some code that represents which types of roles
* can access different actions on users in our database.
*
* It's a relatively simple role-based access system.
*/
const userAccessModel = {
// ^ ๐
user: ["update-self", "view"],
admin: ["create", "update-self", "update-any", "delete", "view"],
anonymous: ["view"],
} as const;
/**
* ๐ Hover userAccessModel. We know what as const does - it freezes
* an object and makes it readonly. We can see from this inference
* that it ALSO does the same to arrays. We're getting inference on
* all of the members of each array.
*
* ๐ต๏ธโโ๏ธ Try removing and re-adding as const, and see what happens
* to the inference.
*/
export type Role = keyof typeof userAccessModel;
/** ^ ๐
*
* ๐ Hover Role. We know about this, too. We're grabbing
* the keys of userAccessModel by first turning it into
* a type (with typeof), then grabbing the keys with keyof.
*/
export type Action = typeof userAccessModel[Role][number];
/** ^ ๐
*
* ๐ Hover Action. This is... interesting. What's [number]
* doing there? We'll get to it later.
*/
export const canUserAccess = (role: Role, action: Action) => {
return (userAccessModel[role] as ReadonlyArray<Action>).includes(action);
/**
* ๐ต๏ธโโ๏ธ Hmmm, the ReadonlyArray<Action> looks a bit scary. Try
* removing it to see what happens.
*
* โ๏ธ Oh dear.
*
* Argument of type '"update-self" | "view" | "create" |
* "update-any" | "delete"' is not assignable to parameter
* of type '"view"'.
*
* ๐ Let's just... put that back in.
*
* return (userAccessModel[role] as ReadonlyArray<Action>).includes(action);
*/
};
/**
* ๐ก So, there's some stuff to figure out here. Let's figure out
* the [number] syntax first.
*
* ๐ Comment out the Action type declared above, and re-add it
* as unknown:
*
* type Action = unknown;
*/
/**
* ๐ก We can figure out the first couple of pieces of
* typeof userAccessModel[Role][number].
*
* ๐ Create a type called UserAccessModelValues, which is assigned
* to userAccessModel[Role]:
*
* type UserAccessModelValues = typeof userAccessModel[Role];
* ^ ๐
*
* ๐ UserAccessModelValues is a union type of all of the values
* of our object. This is similar to the Obj[keyof Obj] setup
* we saw in the apiMapping exercise.
*
* | readonly ["update-self", "view"]
* | readonly ["create", "update-self", "update-any", "delete", "view"]
* | readonly ["view"]
*/
/**
* ๐ก When we want to access a member of an array, we can treat
* it in the same way we would an object. Let's try it.
*
* ๐ Change Action so that it accesses the first member of
* UserAccessModelValues.
*
* type Action = UserAccessModelValues[0];
* ^ ๐
*
* ๐ Hover Action. Now, we're getting the FIRST member of each
* of the arrays in the object.
*/
/**
* ๐ก Now, we could do the same thing as we did in the apiMapping
* exercise, by specifying all of the pieces we want from the arrays:
*
* type Action = UserAccessModelValues[0 | 1 | 2 | 3];
*
* But there's a more elegant solution for accessing _all_ the keys:
*
* ๐ Change Action so that it uses number to access UserAccessModelValues:
*
* type Action = UserAccessModelValues[number];
* ^ ๐
*
* ๐ Hover Action. Now, we're getting ALL members of ALL of the arrays.
*
* ๐ต๏ธโโ๏ธ Discuss amongst yourselves WHY you think [number] works. When you
* think you've figured something out, check:
*
* Solution #1
*/
/**
* ๐ก Now, let's take a look at the scary error we were experiencing
* when removing as ReadonlyArray<Action>.
*
* ๐ Remove as ReadonlyArray<Action>:
*
* return userAccessModel[role].includes(action);
*
* โ๏ธ Guess who's back:
*
* Argument of type '"update-self" | "view" | "create" | "update-any"
* | "delete"' is not assignable to parameter of type '"view"'.
*
* To understand this error, we will have to dig pretty deep.
*
* ๐ Let's look at our UserAccessModelValues type again.
*
* This type is a union of readonly arrays. Some of those arrays
* contain values that aren't present in the others. For instance,
* "anonymous" only contains "view", but "admin" has "delete".
*
* When we call .includes, we're calling it on userAccessModel[role] -
* which is exactly the same type as UserAccessModelValues.
*
* ๐ต๏ธโโ๏ธ Refactor the function so that it saves userAccessModel[role]
* into its own variable before calling includes on it.
*
* const possibleActions = userAccessModel[role];
* ^ ๐
* return possibleActions.includes(action);
*
* ๐ Hover over possibleActions. It should be the same type
* as UserAccessModelValues.
*
* Now that we've confirmed this, we need to look at the type
* definition for .includes to understand what might be happening
* there.
*/
/**
* ๐ฎ Do a find-in-definition on .includes(action)
*
* return possibleActions.includes(action);
* ^ ๐ฎ
*
* You'll be taken to lib.es2016.array.include.d.ts. This is one
* of the files that TypeScript uses to type JavaScript itself!
* We're deep in enemy territory here.
*
* The definition for includes has a searchElement: T. This
* is where our failure appears to be happening.
*
* ๐ต๏ธโโ๏ธ As an investigation, try changing searchElement: T to
* searchElement: any.
*
* includes(searchElement: any, fromIndex?: number): boolean;
*
* The error disappears! Interesting. Now change it back, before
* anyone notices.
*
* ๐ก OK, we've now learned that the error is something to do with
* searchElement: T. At this point in your search, it's time to
* make a decision. We know that possibleActions.includes(action)
* is correct, because both are derived from the config object.
*
* ๐ต๏ธโโ๏ธ Try casting the action to any:
*
* possibleActions.includes(action as any)
*
* It works? Nice.
*
* ๐ต๏ธโโ๏ธ Discuss amongst yourselves: is this a good solution? What
* problems could you imagine coming up against for this? Should
* any _ever_ be used?
*
* For my thoughts, see Solution 2:
*/
/**
* ๐ก Great job! We learned about accessing array members via [number],
* started our find-in-definition journey, and learned that sometimes,
* any is OK.
*/