Skip to content

Latest commit

 

History

History
172 lines (140 loc) · 4.11 KB

RECEPIES.md

File metadata and controls

172 lines (140 loc) · 4.11 KB

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

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

Introducing: createUnion

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

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

// 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
  /* ... */
});

UnionHandlers

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);
  }
});