These code examples are to specific to be part of the project itself but seem valuable in different scenarios.
Assuming we have the following union model configured in cotype
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.
The build-client will type them as:
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
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} />;
})}
</>
);
};
}
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 fromcreateUnion
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
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
// Headline.tsx
import React from "react";
type Props = {
children: ReactNode;
};
export default function Headline({ children }: Props) {
return <h1>{children}</h1>;
}
import Headline from "./Headline";
/* ... */
const FooBar = createUnion<FooBarUnion>({
foo: Headline
/* ... */
});
You can use UnionHandlers
type helper for other (non-react) cases or to
type-safely implement them before passing into createUnion
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);
}
});