1 | # Recepies
|
2 |
|
3 | These code examples are to specific to be part of the project itself
|
4 | but seem valuable in different scenarios.
|
5 |
|
6 | ## Consuming a Union-Model in Typescript+React
|
7 |
|
8 | Assuming we have the following union model configured in cotype
|
9 |
|
10 | ```ts
|
11 | const fooBarUnion = {
|
12 | type: "union",
|
13 | types: {
|
14 | foo: {
|
15 | type: "object",
|
16 | fields: {
|
17 | children: {
|
18 | type: "string"
|
19 | }
|
20 | }
|
21 | },
|
22 | bar: {
|
23 | type: "object",
|
24 | fields: {
|
25 | children: {
|
26 | level: "number"
|
27 | }
|
28 | }
|
29 | }
|
30 | }
|
31 | };
|
32 | ```
|
33 |
|
34 | When consuming the API we will receive an array of objects that either have a
|
35 | `children` property of type `string` or a `level` property of type number.
|
36 | Each entry also brings it's `_type`, which we can use to [discriminate](https://basarat.gitbooks.io/typescript/content/docs/types/discriminated-unions.html).
|
37 |
|
38 | The [build-client](https://github.com/cotype/build-client) will type them as:
|
39 |
|
40 | ```ts
|
41 | type FooBarUnion = Array<
|
42 | { _type: "foo"; children: string } | { _type: "bar"; level: number }
|
43 | >;
|
44 | ```
|
45 |
|
46 | We now want to implement react components that can render each possible type and
|
47 | use only the existing properties.
|
48 |
|
49 | We want to receive type errors in the following scenarios:
|
50 |
|
51 | - React component requires a prop not provided by the api
|
52 | - API provides a type but no component is configured to render it
|
53 | - API type changes but we did not update the component
|
54 |
|
55 | ### Introducing: `createUnion`
|
56 |
|
57 | ```tsx
|
58 | import React, { ReactElement } from "react";
|
59 |
|
60 | type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
61 | type Model = {
|
62 | _type: string;
|
63 | [key: string]: any;
|
64 | };
|
65 | export type UnionHandlers<S extends Model[], R = void> = {
|
66 | [k in S[number]["_type"]]: (
|
67 | props: Omit<Extract<S[number], { _type: k }>, "_type">
|
68 | ) => R
|
69 | };
|
70 |
|
71 | export default function createUnion<S extends Model[]>(
|
72 | comps: UnionHandlers<S, ReactElement>
|
73 | ) {
|
74 | return function Union({ children }: { children: S }) {
|
75 | return (
|
76 | <>
|
77 | {children.map(({ _type, ...props }, i) => {
|
78 | const Comp = comps[_type];
|
79 | /* We need to use any since `props` is still a union of all possible
|
80 | models because we have not explicitly discriminated the model.
|
81 | But since comps is securely typed, we know that `Comp` can handle
|
82 | this exact props. */
|
83 | return <Comp key={i} {...props as any} />;
|
84 | })}
|
85 | </>
|
86 | );
|
87 | };
|
88 | }
|
89 | ```
|
90 |
|
91 | ### Usage
|
92 |
|
93 | ```tsx
|
94 | import createUnion from "./createUnion";
|
95 |
|
96 | type FooBarUnion = Array<
|
97 | { _type: "foo"; children: string } | { _type: "bar"; level: number }
|
98 | >;
|
99 |
|
100 | const FooBar = createUnion<FooBarUnion>({
|
101 | foo({ children }) {
|
102 | return <div>{children}</div>;
|
103 | },
|
104 | bar({ level }) {
|
105 | return <div>{level}</div>;
|
106 | }
|
107 | });
|
108 |
|
109 | function Blog({ sections }: { sections: FooBarUnion }) {
|
110 | return <FooBar>{sections}</FooBar>;
|
111 | }
|
112 | ```
|
113 |
|
114 | We now get errors when:
|
115 |
|
116 | - changing `_type: "foo"` to something else
|
117 | - removing `bar` implementation from `createUnion` parameter
|
118 | - passing nothing or anything else as children into `<FooBar>`
|
119 | - not returning a react element in any of the `createUnion` implementations
|
120 | - Using a parameter that is not provided in the API
|
121 |
|
122 | ### Additional Notes
|
123 |
|
124 | #### Pass reusable components to createUnion
|
125 |
|
126 | We do not need to implement the components directly in the createUnion call.
|
127 | We can also import a reusable component from our library and pass it
|
128 |
|
129 | ```tsx
|
130 | // Headline.tsx
|
131 | import React from "react";
|
132 | type Props = {
|
133 | children: ReactNode;
|
134 | };
|
135 | export default function Headline({ children }: Props) {
|
136 | return <h1>{children}</h1>;
|
137 | }
|
138 | ```
|
139 |
|
140 | ```tsx
|
141 | import Headline from "./Headline";
|
142 | /* ... */
|
143 | const FooBar = createUnion<FooBarUnion>({
|
144 | foo: Headline
|
145 | /* ... */
|
146 | });
|
147 | ```
|
148 |
|
149 | #### UnionHandlers
|
150 |
|
151 | You can use `UnionHandlers` type helper for other (non-react) cases or to
|
152 | type-safely implement them before passing into `createUnion`
|
153 |
|
154 | ```ts
|
155 | import { UnionHandlers } from "./createUnion";
|
156 |
|
157 | const handlers: UnionHandlers<FooBarUnion, any> = {
|
158 | foo: console.log,
|
159 | bar: console.error
|
160 | };
|
161 |
|
162 | const sections: FooBarUnion = await getSections();
|
163 |
|
164 | sections.forEach(section => {
|
165 | switch (section._type) {
|
166 | case "foo":
|
167 | return handlers.foo(section);
|
168 | case "bar":
|
169 | return handlers.bar(section);
|
170 | }
|
171 | });
|
172 | ```
|