11/**
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.
46 */
57
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 ,
816) => {
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 ( " " ) ;
1123 } ;
1224} ;
1325
@@ -16,4 +28,243 @@ const getButtonClasses = createComponent({
1628 secondary : "bg-green-300" ,
1729} ) ;
1830
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