Skip to content

Commit ca1a0bf

Browse files
committed
Finished generics comments
1 parent c9dc93f commit ca1a0bf

16 files changed

+150
-102
lines changed

src/05-generics/31-generic-props.problem.tsx

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { ReactNode } from "react";
2+
import { Equal, Expect } from "../helpers/type-utils";
23

34
interface TableProps {
45
rows: any[];
5-
key: string;
66
renderRow: (row: any) => ReactNode;
77
}
88

9+
/**
10+
* 1. Here, we have a table component. It takes an array of data and a function
11+
* to render each row. The problem is that the type of the data is not
12+
* generic. It's just `any`. We want to make it generic so that the type of
13+
* the data is inferred from the `rows` prop.
14+
*/
915
export const Table = (props: TableProps) => {
1016
return (
1117
<table>
@@ -28,22 +34,11 @@ const data = [
2834
export const Parent = () => {
2935
return (
3036
<div>
37+
<Table rows={data} renderRow={(row) => <td>{row.name}</td>} />
3138
<Table
3239
rows={data}
33-
key="id"
34-
renderRow={(row) => <td>{row.name}</td>}
35-
></Table>
36-
<Table
37-
rows={data}
38-
// @ts-expect-error
39-
key="doesNotExist"
40-
renderRow={(row) => <td>{row.name}</td>}
41-
></Table>
42-
43-
<Table
44-
rows={data}
45-
key="id"
4640
renderRow={(row) => {
41+
type test = Expect<Equal<typeof row, { id: number; name: string }>>;
4742
return (
4843
<td>
4944
{

src/05-generics/31-generic-props.solution.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { ReactNode } from "react";
2+
import { Equal, Expect } from "../helpers/type-utils";
23

34
interface TableProps<T> {
45
rows: T[];
5-
key: keyof T;
66
renderRow: (row: T) => ReactNode;
77
}
88

9+
/**
10+
* The solution is to add a type argument to the function, then
11+
* use that type argument in the type of the `rows` prop and the
12+
* `renderRow` function.
13+
*/
914
export const Table = <T,>(props: TableProps<T>) => {
1015
return (
1116
<table>
@@ -28,22 +33,11 @@ const data = [
2833
export const Parent = () => {
2934
return (
3035
<div>
36+
<Table rows={data} renderRow={(row) => <td>{row.name}</td>} />
3137
<Table
3238
rows={data}
33-
key="id"
34-
renderRow={(row) => <td>{row.name}</td>}
35-
></Table>
36-
<Table
37-
rows={data}
38-
// @ts-expect-error
39-
key="doesNotExist"
40-
renderRow={(row) => <td>{row.name}</td>}
41-
></Table>
42-
43-
<Table
44-
rows={data}
45-
key="id"
4639
renderRow={(row) => {
40+
type test = Expect<Equal<typeof row, { id: number; name: string }>>;
4741
return (
4842
<td>
4943
{

src/05-generics/32-generic-hooks.problem.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { useState } from "react";
22
import { Equal, Expect } from "../helpers/type-utils";
33

4+
/**
5+
* 1. In this exercise, we want to create a version of the useState
6+
* hook that slightly modifies the API - returning it as an object
7+
* instead of a tuple.
8+
*
9+
* There are _many_ different solutions - but they all involve generics.
10+
*/
411
export const useStateAsObject = (initial: any) => {
512
const [value, set] = useState(initial);
613

src/05-generics/32-generic-hooks.solution.1.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { useState } from "react";
22
import { Equal, Expect } from "../helpers/type-utils";
33

4+
/**
5+
* 1. Take a look at each solution, noting the differences between each.
6+
* With some, you might need to do some 'spot the diference' to see
7+
* what's changed.
8+
*
9+
* 2. Which solution do you think is best? Why?
10+
*/
411
export const useStateAsObject = <T>(initial: T) => {
512
const [value, set] = useState(initial);
613

src/05-generics/37-generic-localstorage-hook.problem.ts renamed to src/05-generics/33-generic-localstorage-hook.problem.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
import { it } from "vitest";
22
import { Equal, Expect } from "../helpers/type-utils";
33

4+
/**
5+
* In this exercise, we want to create a generic useLocalStorage hook
6+
* that allows us to store and retrieve values in localStorage.
7+
*
8+
* The way we're going to do this is by asking users to pass in type
9+
* arguments, as below:
10+
*
11+
* const user = useLocalStorage<{ name: string }>("user");
12+
*
13+
* user.set("matt", { name: "Matt" });
14+
*
15+
* 1. Figure out a way to make this work using generics.
16+
*/
417
export const useLocalStorage = (prefix: string) => {
518
return {
619
get: (key: string) => {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Equal, Expect } from "../helpers/type-utils";
2+
3+
interface ButtonGroupProps {
4+
buttons: {
5+
value: string;
6+
label: string;
7+
}[];
8+
onClick: (value: string) => void;
9+
}
10+
11+
/**
12+
* In this exercise, we have a component called ButtonGroup. It takes an array
13+
* of buttons and a function to call when a button is clicked.
14+
*
15+
* We want to improve the type of the onClick function so that the value passed
16+
* to it is inferred from the buttons array.
17+
*
18+
* 1. Try to solve this problem using generics.
19+
*/
20+
const ButtonGroup = (props: ButtonGroupProps) => {
21+
return (
22+
<div>
23+
{props.buttons.map((button) => {
24+
return (
25+
<button
26+
key={button.value}
27+
onClick={() => {
28+
props.onClick(button.value);
29+
}}
30+
>
31+
{button.label}
32+
</button>
33+
);
34+
})}
35+
</div>
36+
);
37+
};
38+
39+
<>
40+
<ButtonGroup
41+
onClick={(value) => {
42+
type test = Expect<Equal<typeof value, "add" | "delete">>;
43+
}}
44+
buttons={[
45+
{
46+
value: "add",
47+
label: "Add",
48+
},
49+
{
50+
value: "delete",
51+
label: "Delete",
52+
},
53+
]}
54+
></ButtonGroup>
55+
</>;
Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,34 @@
11
import { Equal, Expect } from "../helpers/type-utils";
22

3-
interface ButtonGroupProps<TValue extends string> {
4-
buttons: {
5-
value: TValue;
6-
label: string;
7-
}[];
8-
onClick: (value: TValue) => void;
3+
interface ButtonGroupProps<TButtons extends string[]> {
4+
buttons: TButtons;
5+
onClick: (value: TButtons[number]) => void;
96
}
107

11-
const ButtonGroup = <TValue extends string>(
12-
props: ButtonGroupProps<TValue>,
8+
/**
9+
* Here, we've changed the type of the `buttons` prop to be an array of strings.
10+
* But the inference has broken in the ButtonGroup component below!
11+
*
12+
* See if you can find a way to fix it. A 'const' annotation may help:
13+
*
14+
* https://www.totaltypescript.com/const-type-parameters
15+
*
16+
* OR you might be able to fix it by changing the type of the type argument.
17+
*/
18+
const ButtonGroup = <TButtons extends string[]>(
19+
props: ButtonGroupProps<TButtons>,
1320
) => {
1421
return (
1522
<div>
1623
{props.buttons.map((button) => {
1724
return (
1825
<button
19-
key={button.value}
26+
key={button}
2027
onClick={() => {
21-
props.onClick(button.value);
28+
props.onClick(button);
2229
}}
2330
>
24-
{button.label}
31+
{button}
2532
</button>
2633
);
2734
})}
@@ -32,17 +39,12 @@ const ButtonGroup = <TValue extends string>(
3239
<>
3340
<ButtonGroup
3441
onClick={(value) => {
42+
/**
43+
* Instead of inferring the type of `value` to be "add" | "delete", it's
44+
* now inferred to be `string`.
45+
*/
3546
type test = Expect<Equal<typeof value, "add" | "delete">>;
3647
}}
37-
buttons={[
38-
{
39-
value: "add",
40-
label: "Add",
41-
},
42-
{
43-
value: "delete",
44-
label: "Delete",
45-
},
46-
]}
48+
buttons={["add", "delete"]}
4749
></ButtonGroup>
4850
</>;

src/05-generics/36-button-group.problem.tsx

Lines changed: 0 additions & 50 deletions
This file was deleted.

src/05-generics/33-lazy-load-component.problem.tsx renamed to src/05-generics/36-lazy-load-component.problem.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { lazy, Suspense, useMemo } from "react";
22

33
type Props = {
4-
loader: () => Promise<{ default: any }>;
4+
loader: unknown;
55
};
66

77
/**
@@ -10,14 +10,16 @@ type Props = {
1010
*
1111
* But it's not typed correctly, and it's not generic enough.
1212
* Fix the typing errors, and make it generic enough to support any component.
13-
* Hint - React.ComponentProps will come in handy!
13+
*
14+
* Hint - React.ComponentProps will come in handy! The solution also includes
15+
* one 'as any'.
1416
*/
1517
function LazyLoad({ loader, ...props }: Props) {
1618
const LazyComponent = useMemo(() => lazy(loader), [loader]);
1719

1820
return (
1921
<Suspense fallback={"Loading..."}>
20-
<LazyComponent {...(props as any)} />
22+
<LazyComponent {...props} />
2123
</Suspense>
2224
);
2325
}

src/05-generics/34-generics-vs-discriminated-unions.problem.tsx renamed to src/05-generics/37-generics-vs-discriminated-unions.problem.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/**
2+
* In this exercise, we'll look at an example where generics are NOT
3+
* needed.
4+
*
5+
* 1. Take a look at the ModalProps type. Try to figure out what's
6+
* going on in the type.
7+
*
8+
* Notice what type gets returned if you type:
9+
*
10+
* type Example = ModalProps<'with-button'>;
11+
* type Example2 = ModalProps<'without-button'>;
12+
*
13+
* 2. There's a way of writing this type (and the component!) without
14+
* generics that's much simpler. Try to figure out how to do that.
15+
*/
16+
117
export type ModalProps<TVariant extends PossibleVariants> = {
218
isOpen: boolean;
319
variant: TVariant;

src/05-generics/34-generics-vs-discriminated-unions.solution.tsx renamed to src/05-generics/37-generics-vs-discriminated-unions.solution.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
/**
2+
* Yep, a discriminated union is the way to go here. Instead of
3+
* using a generic type (with a conditional type!), we can use
4+
* a discriminated union to make sure that the correct props
5+
* are passed in for each variant.
6+
*/
7+
18
export type ModalProps =
29
| {
310
isOpen: boolean;

0 commit comments

Comments
 (0)