= T | (string & {});
11 |
12 | const looseAutocomplete = (t: any) => {
13 | return "hello";
14 | };
15 |
16 | export const icons: LooseIcon[] = [
17 | "home",
18 | "settings",
19 | "about",
20 | "any-other-string",
21 | // I should get autocomplete if I add a new item here!
22 | ];
23 |
24 | export const buttonVariants: LooseButtonVariant[] = [
25 | "primary",
26 | "secondary",
27 | "tertiary",
28 | "any-other-string",
29 | // I should get autocomplete if I add a new item here!
30 | ];
31 |
--------------------------------------------------------------------------------
/src/07-types-deep-dive/57-react-component-type.explainer.tsx:
--------------------------------------------------------------------------------
1 | type types = [React.ElementType, React.ComponentType];
2 |
3 | /**
4 | * ElementType
5 | *
6 | * Lets you specify certain types of elements
7 | * which can receive those props.
8 | *
9 | * For instance, Example accepts 'audio' and 'video'!
10 | * As well as ComponentType
11 | */
12 | export type Example = React.ElementType<{
13 | autoPlay?: boolean;
14 | }>;
15 |
16 | /**
17 | * ComponentType
18 | */
19 | const FuncComponent = (props: { prop1: string }) => {
20 | return null;
21 | };
22 |
23 | class ClassComponent extends React.Component<{
24 | prop1: string;
25 | }> {
26 | render(): React.ReactNode {
27 | this.props.prop1;
28 | return null;
29 | }
30 | }
31 |
32 | const tests2: Array> = [
33 | FuncComponent,
34 | ClassComponent,
35 | ];
36 |
--------------------------------------------------------------------------------
/src/04-advanced-props/30-partial-autocomplete.solution.tsx:
--------------------------------------------------------------------------------
1 | const presetSizes = {
2 | xs: "0.5rem",
3 | sm: "1rem",
4 | };
5 |
6 | type Size = keyof typeof presetSizes;
7 |
8 | /**
9 | * Oddly, this works. Forcing string to intersect with {} does SOMETHING
10 | * which makes TypeScript do what we want.
11 | *
12 | * Honestly, I'm not sure why this works. Some compiler-diving is required
13 | * to figure it out.
14 | */
15 | type LooseSize = Size | (string & {});
16 |
17 | export const Icon = (props: { size: LooseSize }) => {
18 | return (
19 |
27 | );
28 | };
29 |
30 | <>
31 |
32 |
33 |
34 | >;
35 |
--------------------------------------------------------------------------------
/src/08-advanced-patterns/67-hoc.problem.tsx:
--------------------------------------------------------------------------------
1 | import { Router, useRouter } from "fake-external-lib";
2 |
3 | export const withRouter = (Component: any) => {
4 | const NewComponent = (props: any) => {
5 | const router = useRouter();
6 | return ;
7 | };
8 |
9 | NewComponent.displayName = `withRouter(${Component.displayName})`;
10 |
11 | return NewComponent;
12 | };
13 |
14 | const UnwrappedComponent = (props: { router: Router; id: string }) => {
15 | return null;
16 | };
17 |
18 | const WrappedComponent = withRouter(UnwrappedComponent);
19 |
20 | <>
21 | {/* @ts-expect-error needs a router! */}
22 |
23 |
24 | {/* Doesn't need a router passed in! */}
25 |
26 |
27 |
31 | >;
32 |
--------------------------------------------------------------------------------
/src/08-advanced-patterns/65-forward-ref-with-generics.explainer.2.tsx:
--------------------------------------------------------------------------------
1 | import { ForwardedRef, forwardRef } from "react";
2 | import { Equal, Expect } from "../helpers/type-utils";
3 |
4 | type Props = {
5 | data: T[];
6 | renderRow: (item: T) => React.ReactNode;
7 | };
8 |
9 | /**
10 | * It even works across module boundaries!
11 | *
12 | * Try uncommenting the declare module section in
13 | * explainer.1.tsx, and watch the error below go away.
14 | */
15 | export const Table = (props: Props, ref: ForwardedRef) => {
16 | return null;
17 | };
18 |
19 | const ForwardReffedTable = forwardRef(Table);
20 |
21 | const Parent = () => {
22 | return (
23 | {
26 | type test = Expect>;
27 | return 123
;
28 | }}
29 | >
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/05-generics/38-generic-hooks.solution.2.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Equal, Expect } from "../helpers/type-utils";
3 |
4 | export const useStateAsObject = (
5 | initial: T,
6 | ): {
7 | value: T;
8 | set: React.Dispatch>;
9 | } => {
10 | const [value, set] = useState(initial);
11 |
12 | return {
13 | value,
14 | set,
15 | };
16 | };
17 |
18 | const example = useStateAsObject({ name: "Matt" });
19 |
20 | type ExampleTests = [
21 | Expect>,
22 | Expect<
23 | Equal<
24 | typeof example.set,
25 | React.Dispatch>
26 | >
27 | >,
28 | ];
29 |
30 | const num = useStateAsObject(2);
31 |
32 | type NumTests = [
33 | Expect>,
34 | Expect>>>,
35 | ];
36 |
--------------------------------------------------------------------------------
/src/07-types-deep-dive/56-understanding-jsx-intrinsic-elements.problem.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 1. What is JSX.IntrinsicElements? CMD-click on .IntrinsicElements below
3 | * to go to its definition.
4 | *
5 | * Hint - remember to go to the original definition of JSX.IntrinsicElements
6 | * in @types/react/index.d.ts.
7 | */
8 |
9 | export type Example = JSX.IntrinsicElements;
10 |
11 | /**
12 | * 2. What is the structure of JSX.IntrinsicElements? It appears to have the
13 | * HTML attributes as properties, but what are the values?
14 | *
15 | * 3. Let's have some fun. Edit the file to add a new property to
16 | * JSX.IntrinsicElements:
17 | *
18 | * interface IntrinsicElements {
19 | * // ...
20 | * myNewElement: {
21 | * foo: string;
22 | * }
23 | * }
24 | *
25 | * Notice that the error below goes away!
26 | *
27 | * 4. Now change it back, before anyone notices.
28 | */
29 |
30 | ;
31 |
--------------------------------------------------------------------------------
/src/07-types-deep-dive/56-understanding-jsx-intrinsic-elements.solution.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 1. What is JSX.IntrinsicElements? CMD-click on .IntrinsicElements below
3 | * to go to its definition.
4 | *
5 | * Hint - remember to go to the original definition of JSX.IntrinsicElements
6 | * in @types/react/index.d.ts.
7 | */
8 |
9 | export type Example = JSX.IntrinsicElements;
10 |
11 | /**
12 | * 2. What is the structure of JSX.IntrinsicElements? It appears to have the
13 | * HTML attributes as properties, but what are the values?
14 | *
15 | * 3. Let's have some fun. Edit the file to add a new property to
16 | * JSX.IntrinsicElements:
17 | *
18 | * interface IntrinsicElements {
19 | * // ...
20 | * myNewElement: {
21 | * foo: string;
22 | * }
23 | * }
24 | *
25 | * Notice that the error below goes away!
26 | *
27 | * 4. Now change it back, before anyone notices.
28 | */
29 |
30 | ;
31 |
--------------------------------------------------------------------------------
/src/06-advanced-hooks/50-use-state-overloads.solution.2.ts:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | /**
4 | * This approach also works - specifying defaultString?: string | undefined
5 | * in the second overload.
6 | */
7 |
8 | function maybeReturnsString(defaultString: string): string;
9 | function maybeReturnsString(
10 | defaultString?: string | undefined
11 | ): string | undefined;
12 | function maybeReturnsString(defaultString?: string) {
13 | // If you pass a string, it always returns a string
14 | if (defaultString) {
15 | return defaultString;
16 | }
17 |
18 | // Otherwise, it MIGHT return a string or undefined
19 | return Math.random() > 0.5 ? "hello" : undefined;
20 | }
21 |
22 | const example1 = maybeReturnsString("hello");
23 | const example2 = maybeReturnsString(undefined);
24 |
25 | type tests = [
26 | Expect>,
27 | Expect>
28 | ];
29 |
--------------------------------------------------------------------------------
/src/04-advanced-props/27-either-all-these-props-or-none.solution.tsx:
--------------------------------------------------------------------------------
1 | import { ChangeEventHandler } from "react";
2 |
3 | type InputProps = (
4 | | {
5 | value: string;
6 | onChange: ChangeEventHandler;
7 | }
8 | | {
9 | value?: undefined;
10 | onChange?: undefined;
11 | }
12 | ) & {
13 | label: string;
14 | };
15 |
16 | export const Input = ({ label, ...props }: InputProps) => {
17 | return (
18 |
19 |
23 |
24 | );
25 | };
26 |
27 | export const Test = () => {
28 | return (
29 |
30 | {}} />
31 |
32 |
33 | {/* @ts-expect-error */}
34 |
35 |
36 | {/* @ts-expect-error */}
37 | {}} />
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/08-advanced-patterns/67-hoc.solution.tsx:
--------------------------------------------------------------------------------
1 | import { Router, useRouter } from "fake-external-lib";
2 |
3 | export const withRouter = (Component: React.ComponentType) => {
4 | const NewComponent = (props: Omit) => {
5 | const router = useRouter();
6 | return ;
7 | };
8 |
9 | NewComponent.displayName = `withRouter(${Component.displayName})`;
10 |
11 | return NewComponent;
12 | };
13 |
14 | const UnwrappedComponent = (props: { router: Router; id: string }) => {
15 | return null;
16 | };
17 |
18 | const WrappedComponent = withRouter(UnwrappedComponent);
19 |
20 | <>
21 | {/* @ts-expect-error needs a router! */}
22 |
23 |
24 | {/* Doesn't need a router passed in! */}
25 |
26 |
27 |
31 | >;
32 |
--------------------------------------------------------------------------------
/src/04-advanced-props/23-destructuring-discriminated-unions.solution.1.tsx:
--------------------------------------------------------------------------------
1 | type ModalProps =
2 | | {
3 | variant: "no-title";
4 | }
5 | | {
6 | variant: "title";
7 | title: string;
8 | };
9 |
10 | /**
11 | * Sadly, this doesn't work - TypeScript can't figure out from narrowing
12 | * the destructured variant that the title is now available.
13 | */
14 | export const Modal = ({ variant, ...props }: ModalProps) => {
15 | if (variant === "no-title") {
16 | return No title
;
17 | } else {
18 | return Title: {props.title}
;
19 | }
20 | };
21 |
22 | export const Test = () => {
23 | return (
24 |
25 |
26 |
27 |
28 | {/* @ts-expect-error */}
29 |
30 |
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/05-generics/38-generic-hooks.solution.3.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Equal, Expect } from "../helpers/type-utils";
3 |
4 | type UseStateAsObjectReturn = {
5 | value: T;
6 | set: React.Dispatch>;
7 | };
8 |
9 | export const useStateAsObject = (initial: T): UseStateAsObjectReturn => {
10 | const [value, set] = useState(initial);
11 |
12 | return {
13 | value,
14 | set,
15 | };
16 | };
17 |
18 | const example = useStateAsObject({ name: "Matt" });
19 |
20 | type ExampleTests = [
21 | Expect>,
22 | Expect<
23 | Equal<
24 | typeof example.set,
25 | React.Dispatch>
26 | >
27 | >,
28 | ];
29 |
30 | const num = useStateAsObject(2);
31 |
32 | type NumTests = [
33 | Expect>,
34 | Expect>>>,
35 | ];
36 |
--------------------------------------------------------------------------------
/src/05-generics/38-generic-hooks.solution.4.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Equal, Expect } from "../helpers/type-utils";
3 |
4 | interface UseStateAsObjectReturn {
5 | value: T;
6 | set: React.Dispatch>;
7 | }
8 |
9 | export const useStateAsObject = (initial: T): UseStateAsObjectReturn => {
10 | const [value, set] = useState(initial);
11 |
12 | return {
13 | value,
14 | set,
15 | };
16 | };
17 |
18 | const example = useStateAsObject({ name: "Matt" });
19 |
20 | type ExampleTests = [
21 | Expect>,
22 | Expect<
23 | Equal<
24 | typeof example.set,
25 | React.Dispatch>
26 | >
27 | >,
28 | ];
29 |
30 | const num = useStateAsObject(2);
31 |
32 | type NumTests = [
33 | Expect>,
34 | Expect>>>,
35 | ];
36 |
--------------------------------------------------------------------------------
/src/08-advanced-patterns/63-lazy-load-component.solution.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps, ComponentType, lazy, Suspense, useMemo } from "react";
2 |
3 | type Props> = {
4 | loader: () => Promise<{
5 | default: C;
6 | }>;
7 | } & ComponentProps;
8 |
9 | function LazyLoad>({
10 | loader,
11 | ...props
12 | }: Props) {
13 | const LazyComponent = useMemo(() => lazy(loader), [loader]);
14 |
15 | return (
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | <>
23 | import("fake-external-component")} id="123" />
24 |
25 | import("fake-external-component")}
27 | // @ts-expect-error number is not assignable to string
28 | id={123}
29 | />
30 |
31 | {/* @ts-expect-error id is missing! */}
32 | import("fake-external-component")} />
33 | >;
34 |
--------------------------------------------------------------------------------
/src/05-generics/35-type-helpers-2.solution.tsx:
--------------------------------------------------------------------------------
1 | import { ChangeEventHandler } from "react";
2 |
3 | type AllOrNothing = T | ToUndefinedObject;
4 |
5 | type ToUndefinedObject = Partial>;
6 |
7 | export type InputProps = AllOrNothing<{
8 | value: string;
9 | onChange: ChangeEventHandler;
10 | }> & {
11 | label: string;
12 | };
13 |
14 | export const Input = ({ label, ...props }: InputProps) => {
15 | return (
16 |
17 |
21 |
22 | );
23 | };
24 |
25 | export const Test = () => {
26 | return (
27 |
28 | {}} />
29 |
30 |
31 | {/* @ts-expect-error */}
32 |
33 |
34 | {/* @ts-expect-error */}
35 | {}} />
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/05-generics/37-generic-localstorage-hook.solution.ts:
--------------------------------------------------------------------------------
1 | import { it } from "vitest";
2 | import { Equal, Expect } from "../helpers/type-utils";
3 |
4 | export const useLocalStorage = (prefix: string) => {
5 | return {
6 | get: (key: string): T | null => {
7 | return JSON.parse(window.localStorage.getItem(prefix + key) || "null");
8 | },
9 | set: (key: string, value: T) => {
10 | window.localStorage.setItem(prefix + key, JSON.stringify(value));
11 | },
12 | };
13 | };
14 |
15 | const user = useLocalStorage<{ name: string }>("user");
16 |
17 | it("Should let you set and get values", () => {
18 | user.set("matt", { name: "Matt" });
19 |
20 | const mattUser = user.get("matt");
21 |
22 | type tests = [Expect>];
23 | });
24 |
25 | it("Should not let you set a value that is not the same type as the type argument passed", () => {
26 | user.set(
27 | "something",
28 | // @ts-expect-error
29 | {},
30 | );
31 | });
32 |
--------------------------------------------------------------------------------
/src/04-advanced-props/28-passing-react-components-vs-passing-react-nodes.solution.2.tsx:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | /**
4 | * We can also use React.FC in this position to fix the errors.
5 | */
6 | interface TableProps {
7 | renderRow: React.FC;
8 | }
9 |
10 | const Table = (props: TableProps) => {
11 | return {[0, 1, 3].map(props.renderRow)}
;
12 | };
13 |
14 | export const Parent = () => {
15 | return (
16 | <>
17 | {
19 | type test = Expect>;
20 | return {index}
;
21 | }}
22 | />
23 | {
25 | return null;
26 | }}
27 | />
28 | }
31 | />
32 | {
34 | return index;
35 | }}
36 | />
37 | >
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/06-advanced-hooks/52-currying-hooks.solution.ts:
--------------------------------------------------------------------------------
1 | import { DependencyList, useMemo, useState } from "react";
2 | import { Equal, Expect } from "../helpers/type-utils";
3 |
4 | const useCustomState = (initial: TValue) => {
5 | const [value, set] = useState(initial);
6 |
7 | return {
8 | value,
9 | set,
10 | /**
11 | * We can use a generic _inline_ here to ensure
12 | * this all still works.
13 | */
14 | useComputed: (
15 | factory: (value: TValue) => TComputed,
16 | deps?: DependencyList,
17 | ) => {
18 | return useMemo(() => {
19 | return factory(value);
20 | }, [value, ...(deps || [])]);
21 | },
22 | };
23 | };
24 |
25 | const Component = () => {
26 | const arrayOfNums = useCustomState([1, 2, 3, 4, 5, 6, 7, 8]);
27 |
28 | const reversedAsString = arrayOfNums.useComputed((nums) =>
29 | Array.from(nums).reverse().map(String),
30 | );
31 |
32 | type test = Expect>;
33 | };
34 |
--------------------------------------------------------------------------------
/src/07-types-deep-dive/55-strongly-typing-children.problem.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | /**
4 | * In this example we have a Select component. Through some magic, we're
5 | * attempting to strongly type the children of the Select component so
6 | * that you can only pass 'Option' elements to it.
7 | *
8 | * 1. Try to understand the type of OptionType. What's the __brand property
9 | * for?
10 | *
11 | * 2. There's an error happening at below. Why is that?
12 | *
13 | * 3. Try changing to {Option()}. This appears to work. Why?
14 | * And why is this NOT a good idea?
15 | *
16 | * 4. Is what we're attempting to do even possible?
17 | */
18 |
19 | type OptionType = {
20 | __brand: "OPTION_TYPE";
21 | } & ReactNode;
22 |
23 | const Option = () => {
24 | return () as OptionType;
25 | };
26 |
27 | const Select = (props: { children: OptionType }) => {
28 | return ;
29 | };
30 |
31 | ;
34 |
--------------------------------------------------------------------------------
/src/07-types-deep-dive/55-strongly-typing-children.solution.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | /**
4 | * In this example we have a Select component. Through some magic, we're
5 | * attempting to strongly type the children of the Select component so
6 | * that you can only pass 'Option' elements to it.
7 | *
8 | * 1. Try to understand the type of OptionType. What's the __brand property
9 | * for?
10 | *
11 | * 2. There's an error happening at below. Why is that?
12 | *
13 | * 3. Try changing to {Option()}. This appears to work. Why?
14 | * And why is this NOT a good idea?
15 | *
16 | * 4. Is what we're attempting to do even possible?
17 | */
18 |
19 | type OptionType = {
20 | __brand: "OPTION_TYPE";
21 | } & ReactNode;
22 |
23 | const Option = () => {
24 | return () as OptionType;
25 | };
26 |
27 | const Select = (props: { children: OptionType }) => {
28 | return ;
29 | };
30 |
31 | ;
34 |
--------------------------------------------------------------------------------
/src/03-hooks/21-use-reducer.solution.1.ts:
--------------------------------------------------------------------------------
1 | import { useReducer } from "react";
2 | import { Equal, Expect } from "../helpers/type-utils";
3 |
4 | const reducer = (
5 | state: {
6 | count: number;
7 | },
8 | action:
9 | | {
10 | type: "add";
11 | add: number;
12 | }
13 | | {
14 | type: "subtract";
15 | subtract: number;
16 | }
17 | ) => {
18 | switch (action.type) {
19 | case "add":
20 | return { count: state.count + action.add };
21 | case "subtract":
22 | return { count: state.count - action.subtract };
23 | default:
24 | throw new Error();
25 | }
26 | };
27 |
28 | const [state, dispatch] = useReducer(reducer, { count: 0 });
29 |
30 | type tests = [Expect>];
31 |
32 | dispatch({ type: "add", add: 1 });
33 |
34 | // @ts-expect-error
35 | dispatch({ type: "SUBTRACT", subtract: 1 });
36 |
37 | // @ts-expect-error
38 | dispatch({ type: "add" });
39 |
40 | // @ts-expect-error
41 | dispatch({ type: "subtract", subtract: "123" });
42 |
--------------------------------------------------------------------------------
/src/04-advanced-props/23-destructuring-discriminated-unions.problem.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * We've got the same problem as the previous exercise, but this time we're
3 | * destructuring our props.
4 | *
5 | * 1. Figure out why the error on 'title' is happening.
6 | *
7 | * 2. Find a way to fix the error.
8 | */
9 |
10 | type ModalProps =
11 | | {
12 | variant: "no-title";
13 | }
14 | | {
15 | variant: "title";
16 | title: string;
17 | };
18 |
19 | export const Modal = ({ variant, title }: ModalProps) => {
20 | if (variant === "no-title") {
21 | return No title
;
22 | } else {
23 | return Title: {title}
;
24 | }
25 | };
26 |
27 | export const Test = () => {
28 | return (
29 |
30 |
31 |
32 |
33 | {/* @ts-expect-error */}
34 |
35 |
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/04-advanced-props/29-variants-with-classnames.solution.tsx:
--------------------------------------------------------------------------------
1 | const classNamesMap = {
2 | primary: "bg-blue-500 text-white",
3 | secondary: "bg-gray-200 text-black",
4 | success: "bg-green-500 text-white",
5 | };
6 |
7 | /**
8 | * By using 'typeof' and 'keyof', we can _derive_ the type of
9 | * variant from the classNamesMap object.
10 | *
11 | * 1. Try adding a new key to classNamesMap, and see how the
12 | * type of variant automatically updates.
13 | */
14 | type ButtonProps = {
15 | variant: keyof typeof classNamesMap;
16 | };
17 |
18 | export const Button = (props: ButtonProps) => {
19 | return ;
20 | };
21 |
22 | const Parent = () => {
23 | return (
24 | <>
25 |
26 |
27 |
28 |
29 | {/* @ts-expect-error */}
30 |
31 | {/* @ts-expect-error */}
32 |
33 | >
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/src/05-generics/38-generic-hooks.solution.1.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Equal, Expect } from "../helpers/type-utils";
3 |
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 | */
11 | export const useStateAsObject = (initial: T) => {
12 | const [value, set] = useState(initial);
13 |
14 | return {
15 | value,
16 | set,
17 | };
18 | };
19 |
20 | const example = useStateAsObject({ name: "Matt" });
21 |
22 | type ExampleTests = [
23 | Expect>,
24 | Expect<
25 | Equal<
26 | typeof example.set,
27 | React.Dispatch>
28 | >
29 | >,
30 | ];
31 |
32 | const num = useStateAsObject(2);
33 |
34 | type NumTests = [
35 | Expect>,
36 | Expect>>>,
37 | ];
38 |
--------------------------------------------------------------------------------
/src/04-advanced-props/23-destructuring-discriminated-unions.solution.2.tsx:
--------------------------------------------------------------------------------
1 | type ModalProps =
2 | | {
3 | variant: "no-title";
4 | }
5 | | {
6 | variant: "title";
7 | title: string;
8 | };
9 |
10 | /**
11 | * The best solution is to destructure AFTER the variant has been narrowed.
12 | *
13 | * This gives TypeScript the chance to apply the narrowing to the 'props' object,
14 | * which it understands that 'variant' is a property of.
15 | */
16 | export const Modal = (props: ModalProps) => {
17 | if (props.variant === "no-title") {
18 | return No title
;
19 | } else {
20 | const { title } = props;
21 | return Title: {title}
;
22 | }
23 | };
24 |
25 | export const Test = () => {
26 | return (
27 |
28 |
29 |
30 |
31 | {/* @ts-expect-error */}
32 |
33 |
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/05-generics/38-generic-hooks.problem.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Equal, Expect } from "../helpers/type-utils";
3 |
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 | */
11 | export const useStateAsObject = (initial: any) => {
12 | const [value, set] = useState(initial);
13 |
14 | return {
15 | value,
16 | set,
17 | };
18 | };
19 |
20 | const example = useStateAsObject({ name: "Matt" });
21 |
22 | type ExampleTests = [
23 | Expect>,
24 | Expect<
25 | Equal<
26 | typeof example.set,
27 | React.Dispatch>
28 | >
29 | >,
30 | ];
31 |
32 | const num = useStateAsObject(2);
33 |
34 | type NumTests = [
35 | Expect>,
36 | Expect>>>,
37 | ];
38 |
--------------------------------------------------------------------------------
/.github/workflows/renovate-checks.yml:
--------------------------------------------------------------------------------
1 | name: Renovate Checks
2 | on:
3 | push:
4 | branches:
5 | - "renovate/**"
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout Main
12 | uses: actions/checkout@v4
13 | with:
14 | ref: main
15 | path: repo
16 |
17 | - name: Install Dependencies in Main
18 | run: (cd repo && npm install)
19 | - name: Create Snapshot In Main
20 | run: (cd repo && npx tt-cli take-snapshot ./snap.md)
21 | - name: Copy Snapshot To Outer Directory
22 | run: mv repo/snap.md ./snap.md
23 | - name: Delete Main Directory
24 | run: rm -rf repo
25 | - name: Checkout Branch
26 | uses: actions/checkout@v4
27 | with:
28 | path: repo
29 | - name: Install Dependencies in Branch
30 | run: (cd repo && npm install)
31 | - name: Move Snapshot To Branch
32 | run: mv ./snap.md repo/snap.md
33 | - name: Compare Snapshot In Branch
34 | run: (cd repo && npx tt-cli compare-snapshot ./snap.md)
35 |
--------------------------------------------------------------------------------
/src/03-hooks/21-use-reducer.solution.2.ts:
--------------------------------------------------------------------------------
1 | import { useReducer } from "react";
2 | import { Equal, Expect } from "../helpers/type-utils";
3 |
4 | type ReducerState = {
5 | count: number;
6 | };
7 |
8 | type ReducerAction =
9 | | {
10 | type: "add";
11 | add: number;
12 | }
13 | | {
14 | type: "subtract";
15 | subtract: number;
16 | };
17 |
18 | const reducer = (state: ReducerState, action: ReducerAction) => {
19 | switch (action.type) {
20 | case "add":
21 | return { count: state.count + action.add };
22 | case "subtract":
23 | return { count: state.count - action.subtract };
24 | default:
25 | throw new Error();
26 | }
27 | };
28 |
29 | const [state, dispatch] = useReducer(reducer, { count: 0 });
30 |
31 | type tests = [Expect>];
32 |
33 | dispatch({ type: "add", add: 1 });
34 |
35 | // @ts-expect-error
36 | dispatch({ type: "SUBTRACT", subtract: 1 });
37 |
38 | // @ts-expect-error
39 | dispatch({ type: "add" });
40 |
41 | // @ts-expect-error
42 | dispatch({ type: "subtract", subtract: "123" });
43 |
--------------------------------------------------------------------------------
/src/03-hooks/21-use-reducer.solution.3.ts:
--------------------------------------------------------------------------------
1 | import { Reducer, useReducer } from "react";
2 | import { Equal, Expect } from "../helpers/type-utils";
3 |
4 | type ReducerState = {
5 | count: number;
6 | };
7 |
8 | type ReducerAction =
9 | | {
10 | type: "add";
11 | add: number;
12 | }
13 | | {
14 | type: "subtract";
15 | subtract: number;
16 | };
17 |
18 | const reducer: Reducer = (state, action) => {
19 | switch (action.type) {
20 | case "add":
21 | return { count: state.count + action.add };
22 | case "subtract":
23 | return { count: state.count - action.subtract };
24 | default:
25 | throw new Error();
26 | }
27 | };
28 |
29 | const [state, dispatch] = useReducer(reducer, { count: 0 });
30 |
31 | type tests = [Expect>];
32 |
33 | dispatch({ type: "add", add: 1 });
34 |
35 | // @ts-expect-error
36 | dispatch({ type: "SUBTRACT", subtract: 1 });
37 |
38 | // @ts-expect-error
39 | dispatch({ type: "add" });
40 |
41 | // @ts-expect-error
42 | dispatch({ type: "subtract", subtract: "123" });
43 |
--------------------------------------------------------------------------------
/src/04-advanced-props/29-variants-with-classnames.problem.tsx:
--------------------------------------------------------------------------------
1 | const classNamesMap = {
2 | primary: "bg-blue-500 text-white",
3 | secondary: "bg-gray-200 text-black",
4 | success: "bg-green-500 text-white",
5 | };
6 |
7 | type ButtonProps = {
8 | /**
9 | * This isn't ideal - we have to manually sync
10 | * the type of variant with the object above.
11 | *
12 | * 1. How do we rearrange this code so that we don't
13 | * have to manually sync the types?
14 | *
15 | * Hint: you'll need 'typeof' and 'keyof'.
16 | */
17 | variant: "primary" | "secondary" | "success";
18 | };
19 |
20 | export const Button = (props: ButtonProps) => {
21 | return ;
22 | };
23 |
24 | const Parent = () => {
25 | return (
26 | <>
27 |
28 |
29 |
30 |
31 | {/* @ts-expect-error */}
32 |
33 | {/* @ts-expect-error */}
34 |
35 | >
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/04-advanced-props/22-discriminated-union-props.problem.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 1. Currently, ModalProps lets you pass in various impossible combinations of props.
3 | *
4 | * For instance, you can pass in a `variant` of "title" without passing in a title,
5 | * or you can pass in a `variant` of "no-title" WITH a title.
6 | *
7 | * Try to find a way to express ModalProps so that it's impossible to pass in
8 | * impossible combinations of props.
9 | */
10 |
11 | type ModalProps = {
12 | variant: "no-title" | "title";
13 | title?: string;
14 | };
15 |
16 | export const Modal = (props: ModalProps) => {
17 | if (props.variant === "no-title") {
18 | return No title
;
19 | } else {
20 | return Title: {props.title}
;
21 | }
22 | };
23 |
24 | export const Test = () => {
25 | return (
26 |
27 |
28 |
29 |
30 | {/* @ts-expect-error */}
31 |
32 |
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/04-advanced-props/28-passing-react-components-vs-passing-react-nodes.solution.1.tsx:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | /**
4 | * The errors are happening because the dev didn't understand what
5 | * React.ReactNode was for.
6 | *
7 | * By changing it to a function which returns a React.ReactNode,
8 | * we can fix the errors.
9 | */
10 |
11 | interface TableProps {
12 | renderRow: (rowIndex: number) => React.ReactNode;
13 | }
14 |
15 | const Table = (props: TableProps) => {
16 | return {[0, 1, 3].map(props.renderRow)}
;
17 | };
18 |
19 | export const Parent = () => {
20 | return (
21 | <>
22 | {
24 | type test = Expect>;
25 | return {index}
;
26 | }}
27 | />
28 | {
30 | return null;
31 | }}
32 | />
33 | }
36 | />
37 | {
39 | return index;
40 | }}
41 | />
42 | >
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/src/05-generics/42-generic-inference-through-multiple-helpers.solution.tsx:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | interface Button {
4 | value: TValue;
5 | label: string;
6 | }
7 |
8 | interface ButtonGroupProps {
9 | buttons: Button[];
10 | onClick: (value: TValue) => void;
11 | }
12 |
13 | const ButtonGroup = (
14 | props: ButtonGroupProps
15 | ) => {
16 | return (
17 |
18 | {props.buttons.map((button) => {
19 | return (
20 |
28 | );
29 | })}
30 |
31 | );
32 | };
33 |
34 | <>
35 | {
37 | type test = Expect>;
38 | }}
39 | buttons={[
40 | {
41 | value: "add",
42 | label: "Add",
43 | },
44 | {
45 | value: "delete",
46 | label: "Delete",
47 | },
48 | ]}
49 | >
50 | >;
51 |
--------------------------------------------------------------------------------
/src/06-advanced-hooks/49-discriminated-tuples-from-custom-hooks.solution.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | /**
4 | * A discriminated tuple!
5 | *
6 | * The really cool thing about this is that TypeScript can infer the type
7 | * even after it's been destructured.
8 | */
9 | export type Result =
10 | | ["loading", undefined?]
11 | | ["success", T]
12 | | ["error", Error];
13 |
14 | export const useData = (url: string): Result => {
15 | const [result, setResult] = useState>(["loading"]);
16 |
17 | useEffect(() => {
18 | fetch(url)
19 | .then((response) => response.json())
20 | .then((data) => setResult(["success", data]))
21 | .catch((error) => setResult(["error", error]));
22 | }, [url]);
23 |
24 | return result;
25 | };
26 |
27 | const Component = () => {
28 | const [status, value] = useData<{ title: string }>(
29 | "https://jsonplaceholder.typicode.com/todos/1"
30 | );
31 |
32 | if (status === "loading") {
33 | return Loading...
;
34 | }
35 |
36 | if (status === "error") {
37 | return Error: {value.message}
;
38 | }
39 |
40 | return {value.title}
;
41 | };
42 |
--------------------------------------------------------------------------------
/src/04-advanced-props/25-toggle-props.solution.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * This is just another kind of discriminated union - one based on a
3 | * boolean, not a string.
4 | *
5 | * Note that we use an optional property for the boolean in the
6 | * CodeSandbox version, because if the user doesn't specify it (passes
7 | * undefined), we want to default to it.
8 | */
9 | type EmbeddedPlaygroundProps =
10 | | {
11 | useStackblitz: true;
12 | stackblitzId: string;
13 | }
14 | | {
15 | useStackblitz?: false;
16 | codeSandboxId: string;
17 | };
18 |
19 | const EmbeddedPlayground = (props: EmbeddedPlaygroundProps) => {
20 | if (props.useStackblitz) {
21 | return (
22 |
25 | );
26 | }
27 |
28 | return ;
29 | };
30 |
31 | <>
32 |
33 |
34 |
35 |
40 | >;
41 |
--------------------------------------------------------------------------------
/src/08-advanced-patterns/64.5-record-of-components-with-same-props.solution.1.tsx:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | type InputProps = React.ComponentProps<"input">;
4 |
5 | type Input = "text" | "number" | "password";
6 |
7 | /**
8 | * We can do it by typing Input and making COMPONENTS
9 | * restricted to only those inputs.
10 | */
11 | const COMPONENTS: Record> = {
12 | text: (props) => {
13 | return ;
14 | },
15 | number: (props) => {
16 | return ;
17 | },
18 | password: (props) => {
19 | return ;
20 | },
21 | };
22 |
23 | export const Input = (props: { type: Input } & InputProps) => {
24 | const Component = COMPONENTS[props.type];
25 | return ;
26 | };
27 |
28 | <>
29 | {
32 | type test = Expect>>;
33 | }}
34 | >
35 |
36 |
37 |
38 | {/* @ts-expect-error */}
39 |
40 | >;
41 |
--------------------------------------------------------------------------------
/src/04-advanced-props/28-passing-react-components-vs-passing-react-nodes.problem.tsx:
--------------------------------------------------------------------------------
1 | import { Equal, Expect } from "../helpers/type-utils";
2 |
3 | /**
4 | * 1. We've got a bunch of different errors below. See if you can figure
5 | * out why the errors are happening.
6 | *
7 | * 2. Once you understand why the errors are happening, see if you can
8 | * find a way to fix them by changing the definition of TableProps.
9 | */
10 | interface TableProps {
11 | renderRow: React.ReactNode;
12 | }
13 |
14 | const Table = (props: TableProps) => {
15 | return {[0, 1, 3].map(props.renderRow)}
;
16 | };
17 |
18 | export const Parent = () => {
19 | return (
20 | <>
21 | {
23 | type test = Expect>;
24 | return {index}
;
25 | }}
26 | />
27 | {
29 | return null;
30 | }}
31 | />
32 | }
35 | />
36 | {
38 | return index;
39 | }}
40 | />
41 | >
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/src/09-external-libraries/75-react-select.solution.tsx:
--------------------------------------------------------------------------------
1 | import ReactSelect, { GroupBase, Props } from "react-select";
2 | import { Equal, Expect } from "../helpers/type-utils";
3 |
4 | export const Select = <
5 | Option = unknown,
6 | IsMulti extends boolean = false,
7 | Group extends GroupBase