Skip to content

Commit 08aeac2

Browse files
committed
Completed 7
1 parent be17ebb commit 08aeac2

File tree

5 files changed

+351
-14
lines changed

5 files changed

+351
-14
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Each exercise follows a similar pattern:
6060

6161
- 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.
6262
- 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.
63+
- Wherever you see reference to `Solution #1`, check the `*.solutions.ts` file when you want to see the solution. **Make sure you check the solution before proceeding!** There's often crucial information there.
6464

6565
### Emoji
6666

exercises/07-createComponent.code.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
export const createComponent = <TComponent extends Record<string, string>>(
2-
component: TComponent,
1+
export const createComponent = <TConfig extends Record<string, string>>(
2+
config: TConfig,
33
) => {
4-
return (variant: keyof TComponent, ...otherClasses: string[]): string => {
5-
return [component[variant], otherClasses].join(" ");
4+
return (variant: keyof TConfig, ...otherClasses: string[]): string => {
5+
return [config[variant], otherClasses].join(" ");
66
};
77
};
88

@@ -11,4 +11,4 @@ const getButtonClasses = createComponent({
1111
secondary: "bg-green-300",
1212
});
1313

14-
const classes = getButtonClasses("primary");
14+
const classes = getButtonClasses("primary", "px-4 py-2");
Lines changed: 258 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
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+
*/
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* #1
3+
*
4+
* 💡 Observation 1: Anything you pass into the createComponent
5+
* config object becomes a possible variant you can then call
6+
* in getButtonClasses.
7+
*
8+
* const getButtonClasses = createComponent({
9+
* primary: "bg-blue-300",
10+
* secondary: "bg-green-300",
11+
* });
12+
*
13+
* const classes = getButtonClasses("tertiary");
14+
* ^ ⛔️
15+
*
16+
* You'll see an error when passing something that doesn't
17+
* exist in the config. You'll also see that you get
18+
* autocomplete in the first argument of getButtonClasses.
19+
*
20+
* 💡 Observation 2: You'll notice that you'll get an error if
21+
* you pass in non-strings into the values of the config in
22+
* createComponent:
23+
*
24+
* const getButtonClasses = createComponent({
25+
* primary: 1,
26+
* ^ ⛔️
27+
* secondary: "bg-green-300",
28+
* });
29+
*
30+
* Type 'number' is not assignable to type 'string'.
31+
*
32+
* #2
33+
*
34+
* 💡 The Record type lets us create an object where:
35+
*
36+
* 1. The keys of the object are the first parameter
37+
* 2. The values of the object are the second parameter
38+
*
39+
* export const createComponent = (config: Record<string, string>) => {
40+
*
41+
* #3
42+
*
43+
* This could be solved many ways - here's mine:
44+
*
45+
* export const createComponent = (config: Record<string, string>) => {
46+
* return (variant: string, ...classes: string[]) => {
47+
* return config[variant] + " " + classes.join(" ");
48+
* };
49+
* };
50+
*
51+
* #4
52+
*
53+
* 💡 The reason this isn't being inferred is because we
54+
* haven't USED the generic anywhere in our function
55+
* arguments. So it's unknown, because TypeScript doesn't
56+
* know what we want to store in the generic slot.
57+
*
58+
* 🛠 Change config: Record<string, string> to be TConfig.
59+
*
60+
* ⛔️ You'll see an error:
61+
*
62+
* Element implicitly has an 'any' type because expression
63+
* of type 'string' can't be used to index type 'unknown'.
64+
*
65+
* Don't worry, we'll get to that in a minute.
66+
*
67+
* #5
68+
*
69+
* 💡 This is happening because we've specified you can pass
70+
* ANY string to variant. TypeScript is complaining because
71+
* you can't use string to access properties of TConfig.
72+
*
73+
* There's only one way to tell TypeScript that this is safe
74+
* - by changing variant from string to keyof TConfig.
75+
*
76+
* 🛠 Make the change:
77+
*
78+
* return (variant: keyof TConfig, ...classes: string[]) => {
79+
*
80+
* ✅ The error disappears! Hooray!
81+
*
82+
* #6
83+
*
84+
* export const createComponent = <TConfig extends Record<string, string>>(
85+
* config: TConfig,
86+
* ) => {
87+
*/

0 commit comments

Comments
 (0)