@cotype/core
Version:
<h2 align="center"> <img src="https://cotype.dev/logo.svg" alt="cotype" /> </h2>
173 lines (140 loc) • 4.21 kB
Markdown
# Recepies
These code examples are to specific to be part of the project itself
but seem valuable in different scenarios.
## Consuming a Union-Model in Typescript+React
Assuming we have the following union model configured in cotype
```ts
const fooBarUnion = {
type: "union",
types: {
foo: {
type: "object",
fields: {
children: {
type: "string"
}
}
},
bar: {
type: "object",
fields: {
children: {
level: "number"
}
}
}
}
};
```
When consuming the API we will receive an array of objects that either have a
`children` property of type `string` or a `level` property of type number.
Each entry also brings it's `_type`, which we can use to [discriminate](https://basarat.gitbooks.io/typescript/content/docs/types/discriminated-unions.html).
The [build-client](https://github.com/cotype/build-client) will type them as:
```ts
type FooBarUnion = Array<
{ _type: "foo"; children: string } | { _type: "bar"; level: number }
>;
```
We now want to implement react components that can render each possible type and
use only the existing properties.
We want to receive type errors in the following scenarios:
- React component requires a prop not provided by the api
- API provides a type but no component is configured to render it
- API type changes but we did not update the component
### Introducing: `createUnion`
```tsx
import React, { ReactElement } from "react";
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type Model = {
_type: string;
[key: string]: any;
};
export type UnionHandlers<S extends Model[], R = void> = {
[k in S[number]["_type"]]: (
props: Omit<Extract<S[number], { _type: k }>, "_type">
) => R
};
export default function createUnion<S extends Model[]>(
comps: UnionHandlers<S, ReactElement>
) {
return function Union({ children }: { children: S }) {
return (
<>
{children.map(({ _type, ...props }, i) => {
const Comp = comps[_type];
/* We need to use any since `props` is still a union of all possible
models because we have not explicitly discriminated the model.
But since comps is securely typed, we know that `Comp` can handle
this exact props. */
return <Comp key={i} {...props as any} />;
})}
</>
);
};
}
```
### Usage
```tsx
import createUnion from "./createUnion";
type FooBarUnion = Array<
{ _type: "foo"; children: string } | { _type: "bar"; level: number }
>;
const FooBar = createUnion<FooBarUnion>({
foo({ children }) {
return <div>{children}</div>;
},
bar({ level }) {
return <div>{level}</div>;
}
});
function Blog({ sections }: { sections: FooBarUnion }) {
return <FooBar>{sections}</FooBar>;
}
```
We now get errors when:
- changing `_type: "foo"` to something else
- removing `bar` implementation from `createUnion` parameter
- passing nothing or anything else as children into `<FooBar>`
- not returning a react element in any of the `createUnion` implementations
- Using a parameter that is not provided in the API
### Additional Notes
#### Pass reusable components to createUnion
We do not need to implement the components directly in the createUnion call.
We can also import a reusable component from our library and pass it
```tsx
// Headline.tsx
import React from "react";
type Props = {
children: ReactNode;
};
export default function Headline({ children }: Props) {
return <h1>{children}</h1>;
}
```
```tsx
import Headline from "./Headline";
/* ... */
const FooBar = createUnion<FooBarUnion>({
foo: Headline
/* ... */
});
```
#### UnionHandlers
You can use `UnionHandlers` type helper for other (non-react) cases or to
type-safely implement them before passing into `createUnion`
```ts
import { UnionHandlers } from "./createUnion";
const handlers: UnionHandlers<FooBarUnion, any> = {
foo: console.log,
bar: console.error
};
const sections: FooBarUnion = await getSections();
sections.forEach(section => {
switch (section._type) {
case "foo":
return handlers.foo(section);
case "bar":
return handlers.bar(section);
}
});
```