UNPKG

4.21 kBMarkdownView Raw
1# Recepies
2
3These code examples are to specific to be part of the project itself
4but seem valuable in different scenarios.
5
6## Consuming a Union-Model in Typescript+React
7
8Assuming we have the following union model configured in cotype
9
10```ts
11const 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
34When 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.
36Each entry also brings it's `_type`, which we can use to [discriminate](https://basarat.gitbooks.io/typescript/content/docs/types/discriminated-unions.html).
37
38The [build-client](https://github.com/cotype/build-client) will type them as:
39
40```ts
41type FooBarUnion = Array<
42 { _type: "foo"; children: string } | { _type: "bar"; level: number }
43>;
44```
45
46We now want to implement react components that can render each possible type and
47use only the existing properties.
48
49We 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
58import React, { ReactElement } from "react";
59
60type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
61type Model = {
62 _type: string;
63 [key: string]: any;
64};
65export 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
71export 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
94import createUnion from "./createUnion";
95
96type FooBarUnion = Array<
97 { _type: "foo"; children: string } | { _type: "bar"; level: number }
98>;
99
100const FooBar = createUnion<FooBarUnion>({
101 foo({ children }) {
102 return <div>{children}</div>;
103 },
104 bar({ level }) {
105 return <div>{level}</div>;
106 }
107});
108
109function Blog({ sections }: { sections: FooBarUnion }) {
110 return <FooBar>{sections}</FooBar>;
111}
112```
113
114We 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
126We do not need to implement the components directly in the createUnion call.
127We can also import a reusable component from our library and pass it
128
129```tsx
130// Headline.tsx
131import React from "react";
132type Props = {
133 children: ReactNode;
134};
135export default function Headline({ children }: Props) {
136 return <h1>{children}</h1>;
137}
138```
139
140```tsx
141import Headline from "./Headline";
142/* ... */
143const FooBar = createUnion<FooBarUnion>({
144 foo: Headline
145 /* ... */
146});
147```
148
149#### UnionHandlers
150
151You can use `UnionHandlers` type helper for other (non-react) cases or to
152type-safely implement them before passing into `createUnion`
153
154```ts
155import { UnionHandlers } from "./createUnion";
156
157const handlers: UnionHandlers<FooBarUnion, any> = {
158 foo: console.log,
159 bar: console.error
160};
161
162const sections: FooBarUnion = await getSections();
163
164sections.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```