1
1
/**
2
- * 🧑💻 Here, we've got a piece of frontend code that helps
3
- * us assign CSS classes to variants.
2
+ * 🧑💻 Here, we've got a piece of frontend code that helps us
3
+ * assign CSS classes to variants of components. It's a common
4
+ * pattern in apps which use utility CSS libraries, like
5
+ * Tailwind.
4
6
*/
5
7
6
- export const createComponent = < TComponent extends Record < string , string > > (
7
- component : TComponent ,
8
+ /**
9
+ * 💡 Our first generic function! We'll break down what this
10
+ * means later.
11
+ *
12
+ * There's also a Record type here, I wonder what that means...
13
+ */
14
+ export const createComponent = < TConfig extends Record < string , string > > (
15
+ config : TConfig ,
8
16
) => {
9
- return ( variant : keyof TComponent , ...otherClasses : string [ ] ) : string => {
10
- return component [ variant ] + " " + otherClasses . join ( " " ) ;
17
+ /**
18
+ * 💡 It looks like it returns another function, which takes
19
+ * in both the variant and as many other classes as you like.
20
+ */
21
+ return ( variant : keyof TConfig , ...otherClasses : string [ ] ) : string => {
22
+ return config [ variant ] + " " + otherClasses . join ( " " ) ;
11
23
} ;
12
24
} ;
13
25
@@ -16,4 +28,243 @@ const getButtonClasses = createComponent({
16
28
secondary : "bg-green-300" ,
17
29
} ) ;
18
30
19
- const classes = getButtonClasses ( "primary" ) ;
31
+ const classes = getButtonClasses ( "primary" , "px-4 py-2" ) ;
32
+ /**
33
+ * 🕵️♂️ Time for an investigation. Play around with the two
34
+ * function calls above to see what you can figure out
35
+ * about the API.
36
+ *
37
+ * I've written down two observations - see if you can get
38
+ * them both.
39
+ *
40
+ * Solution #1
41
+ */
42
+
43
+ /**
44
+ * 🛠 OK, let's break this down. Comment out all of the
45
+ * code above.
46
+ *
47
+ * 🛠 Create a function called createComponent. Make that
48
+ * function take in a single argument, config, typed as
49
+ * unknown. Export it.
50
+ *
51
+ * export const createComponent = (config: unknown) => {}
52
+ *
53
+ * 🛠 Make createComponent return another function, which
54
+ * takes in variant: string and returns config[variant].
55
+ *
56
+ * export const createComponent = (config: unknown) => {
57
+ * return (variant: string) => {
58
+ * return config[variant];
59
+ * };
60
+ * };
61
+ *
62
+ * 🛠 You should also adjust the return type so that it
63
+ * appends any other classes you want to pass to
64
+ * config[variant].
65
+ *
66
+ * Solution #3
67
+ */
68
+
69
+ /**
70
+ * ⛔️ Error!
71
+ *
72
+ * return config[variant];
73
+ * ^ ⛔️
74
+ *
75
+ * Object is of type 'unknown'.
76
+ *
77
+ * That's right - we haven't typed our config properly.
78
+ *
79
+ * 🛠 See if you can figure out how to type this correctly,
80
+ * using the Record type from TypeScript:
81
+ *
82
+ * https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type
83
+ *
84
+ * Solution #2
85
+ */
86
+
87
+ /**
88
+ * 🕵️♂️ Try using your new function.
89
+ *
90
+ * const getButtonClasses = createComponent({
91
+ * primary: "bg-blue-300",
92
+ * secondary: "bg-green-300",
93
+ * });
94
+ *
95
+ * const classes = getButtonClasses("primary", "px-4 py-2");
96
+ * ^ 🕵️♂️
97
+ */
98
+
99
+ /**
100
+ * 🕵️♂️ You'll notice we're not getting the same safety as we
101
+ * had in our previous setup. You can pass anything into
102
+ * the first argument of getButtonClasses:
103
+ *
104
+ * const classes = getButtonClasses("awdawkawd", "px-4 py-2");
105
+ *
106
+ * This doesn't seem very helpful or typesafe.
107
+ *
108
+ * 💡 If you picture our function calls, it goes something
109
+ * like this:
110
+ *
111
+ * createComponent(config) =>
112
+ * getButtonClasses(variant, ...classes) =>
113
+ * classes
114
+ *
115
+ * The type of variant is going to be the keys of the config
116
+ * object passed to createComponent. This means there's an
117
+ * "inference dependency" between variant and config. If the
118
+ * type of config changes, the type of variant is also going
119
+ * to need to change.
120
+ *
121
+ * In order to make this work, we need a mechanism for storing
122
+ * what the config is inferred as so that we can use it later
123
+ * in getButtonClasses to type variant.
124
+ *
125
+ * We're going to use generics for this method of storage.
126
+ *
127
+ * https://www.typescriptlang.org/docs/handbook/2/generics.html
128
+ *
129
+ * 💡 Our plan is this:
130
+ *
131
+ * 1. Store config in a generic slot called TConfig
132
+ * 2. Make the type of variant the keys of TConfig
133
+ * 3. Profit.
134
+ */
135
+
136
+ /**
137
+ * 🛠 Let's get this working. Add a generic slot to your
138
+ * createComponent function. Don't change the actual type
139
+ * of config: yet.
140
+ *
141
+ * export const createComponent = <TConfig>(config: Record<string, string>) => {
142
+ *
143
+ * 🚁 Hover over your createComponent call:
144
+ *
145
+ * const getButtonClasses = createComponent({
146
+ * ^ 🚁
147
+ *
148
+ * Whenever you call a generic function, you can use hovers
149
+ * to determine what its generic slots are being inferred as.
150
+ *
151
+ * In this case, its slots are:
152
+ *
153
+ * <unknown>(config: Record<string, string>)
154
+ *
155
+ * I.e. a single slot, typed as unknown.
156
+ *
157
+ * 🕵️♂️ This doesn't seem right. We're passing in a config object,
158
+ * but it's being inferred as unknown. Shouldn't it be being
159
+ * inferred as the config object we're passing in?
160
+ *
161
+ * Discuss among yourselves why this is happening, and how to
162
+ * solve it.
163
+ *
164
+ * Solution #4
165
+ */
166
+
167
+ /**
168
+ * 🚁 Now that you've fixed it, try hovering createComponent({
169
+ * again:
170
+ *
171
+ * const getButtonClasses = createComponent({
172
+ * ^ 🚁
173
+ *
174
+ * You'll now see that we're storing the type of the config in
175
+ * the slot:
176
+ *
177
+ * <{
178
+ * primary: string;
179
+ * secondary: string;
180
+ * }>
181
+ *
182
+ * Hooray! We can then use this to type variant.
183
+ */
184
+
185
+ /**
186
+ * ⛔️ Back to this error:
187
+ *
188
+ * Element implicitly has an 'any' type because expression
189
+ * of type 'string' can't be used to index type 'unknown'.
190
+ *
191
+ * 🕵️♂️ Try working this one out yourselves. Here's some things
192
+ * you've seen:
193
+ *
194
+ * When you didn't assign anything to TConfig, it defaulted
195
+ * to the type of 'unknown'.
196
+ *
197
+ * We've removed the Record<string, string> from the function
198
+ * completely.
199
+ *
200
+ * Solution #5
201
+ */
202
+
203
+ /**
204
+ * 🚁 Now that that's fixed, we've incidentally managed to get
205
+ * something working. Try hovering getButtonClasses:
206
+ *
207
+ * const getButtonClasses = createComponent({
208
+ * primary: "bg-blue-300",
209
+ * secondary: "bg-green-300",
210
+ * });
211
+ *
212
+ * const classes = getButtonClasses("primary", "px-4 py-2");
213
+ * ^ 🚁
214
+ *
215
+ * You'll see that variant is now typed as "primary" | "secondary".
216
+ * It'll give you autocomplete, and it'll also error if you don't
217
+ * pass one of those values.
218
+ */
219
+
220
+ /**
221
+ * 💡 Hooray! Looks like we're nearly done. But there's one
222
+ * thing we haven't covered yet.
223
+ *
224
+ * 🕵️♂️ You'll notice that it's still possible to pass invalid
225
+ * values to createComponent():
226
+ *
227
+ * const getButtonClasses = createComponent({
228
+ * primary: 12,
229
+ * ^ 🕵️♂️
230
+ * secondary: "bg-green-300",
231
+ * });
232
+ *
233
+ * primary should be erroring here because we're passing a
234
+ * number, not a string.
235
+ *
236
+ * 💡 This is tricky. We've said that whatever gets inferred
237
+ * to config should be stored in TConfig, but we've lost the
238
+ * ability to constrain what shape config should be.
239
+ *
240
+ * We can achieve this again with generic constraints:
241
+ *
242
+ * https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints
243
+ *
244
+ * 🛠 Add a constraint to TConfig that matches our desired type.
245
+ *
246
+ * Take a look at the docs for the syntax.
247
+ *
248
+ * Solution #6
249
+ */
250
+
251
+ /**
252
+ * ⛔️ You'll now see an error appearing on primary: 12
253
+ *
254
+ * Type 'number' is not assignable to type 'string'.
255
+ *
256
+ * That's good! That matches the behaviour we were seeing before.
257
+ *
258
+ * 💡 That means we've completed our task! We've got the
259
+ * autocomplete working and we've made it impossible to pass
260
+ * an invalid config to createComponent.
261
+ */
262
+
263
+ /**
264
+ * 💡 Great job! We've covered the basics of generics,
265
+ * constraining generics and the idea of "inference
266
+ * dependencies". When you notice a dependency between
267
+ * two types in a function, it's usually time to start
268
+ * thinking about using a generic (or a function overload,
269
+ * which we'll cover later.)
270
+ */
0 commit comments