Skip to content

Type Safety with Code Generation for Third Party APIs

Posted on:May 26, 2020

Client-side JavaScript applications often need to query third party APIs for populating strings, such as for string translations or for displaying content from a CMS.

If you’re developing with TypeScript, these APIs represent a significant omission in type safety.

This post will explain a technique to introduce type safety when using such APIs in your own code, using code generation and a bit of ingenuity. I will be using Prismic as an example, but as you will see, this technique can also be used with other APIs.


Prismic has a nice UI where anyone can edit content, which will be queried and displayed during runtime without needing to rebuild the app. Here, and elsewhere in this post, I will be focusing on a single type called my_page, containing three fields: header, body, and footer.

Let’s start with a simple example (using npx create-react-app --typescript) to show how Prismic’s JavaScript API is integrated, and what potential problem this particular approach might have:

import React, { useState, useEffect } from "react";
import Prismic from "prismic-javascript";
 
const client = Prismic.client("https://my-project.cdn.prismic.io/api/v2");
 
interface MyPageStrings {
  header: string;
  body: string;
  footer: string;
}
 
function App() {
  const [strings, setStrings] = useState<MyPageStrings>({
    header: "",
    body: "",
    footer: "",
  });
  useEffect(() => {
    const fetchData = async (): Promise<void> => {
      const resp = await client.getSingle("my_page", {});
      if (resp) {
        setStrings(resp.data);
      }
    };
    fetchData();
  }, []);
  return (
    <div>
      <div>{strings.header}</div>
      <div>{strings.body}</div>
      <div>{strings.footer}</div>
    </div>
  );
}
 
export default App;

Looks good enough, but there’s already a bit of boilerplate code creeping in. For larger components, this can add up, causing visual clutter.

What if we could have these interfaces generated for us automatically, instead of writing them by hand, and in this case, colocating it with the component?

The gist of the proposed improvement looks like this:

  1. Query the API as you would otherwise
  2. See what the API shape looks like
  3. Generate interfaces based off Step 2
  4. ???
  5. Get VC funding

Presented in full below is a script that does this for Prismic (generate-prismic-types.js):

const fetch = require("node-fetch");
const util = require("util");
const fs = require("fs");
 
// Prismic APIs
const topLevelQuery = "https://my-project.cdn.prismic.io/api/v2";
const typeQuery = (ref, type) =>
  `https://my-project.cdn.prismic.io/api/v2/documents/search?ref=${ref}&q=[[at(document.type, "${type}")]]`;
const fetchType = (ref, type) =>
  fetch(typeQuery(ref, type)).then(e => e.json());
 
// Utils
const toCamel = s =>
  s.replace(/([-_][a-z])/gi, $1 => $1.toUpperCase()).replace(/_/g, "");
const toPascal = s => s.charAt(0).toUpperCase() + toCamel(s.slice(1));
 
// Code for generating the interfaces
const generateInterface = (name, data) => {
  const returnType = toPascal(name) + "Strings";
  if (data && typeof data === "object") {
    return `
    export interface ${returnType} {
      ${Object.keys(data)
        .map(key => `${key}: string`)
        .join("\n      ")}
    }
 
    const empty${returnType}: ${returnType} = {
      ${Object.keys(data)
        .map(key => `${key}: \"\"`)
        .join(",\n      ")}
    }
 
    export const use${returnType} = (): ${returnType} => usePrismicDocByType<${returnType}>("${name}", empty${returnType})`.replace(
      /^ {4}/gm,
      ""
    );
  }
  return "";
};
 
const main = async () => {
  const { refs, types } = await (await fetch(topLevelQuery)).json();
  const ref = refs.filter(e => e.id === "master")[0].ref;
  const typeNames = Object.keys(types);
 
  const results = await Promise.all(
    typeNames.map(async type => {
      const typeInfo = await fetchType(ref, type);
      if (
        "results" in typeInfo &&
        typeInfo.results.length &&
        "data" in typeInfo.results[0]
      ) {
        return typeInfo.results[0].data || null;
      }
      return null;
    })
  );
 
  const header = `/*
 * This file is generated by generate-prismic-types.js. Do not edit by hand.
 *
 * This file contains types generated by querying the Prismic API.
 */
 
import { useEffect, useState } from "react"
import Prismic from "prismic-javascript"
 
const prismic = Prismic.client("https://my-project.cdn.prismic.io/api/v2")
 
const usePrismicDocByType = <T>(documentType: string, defaultValue: T): T => {
  const [strings, setStrings] = useState<T>(defaultValue)
  useEffect((): void => {
    const fetchData = async () => {
      const resp = await prismic.getSingle(documentType, {})
      if (resp) setStrings(resp.data)
    }
    fetchData()
  }, [documentType])
  return strings
}
`;
 
  // Generate TS types
  const typings = Object.values(results)
    .map((typing, i) => generateInterface(typeNames[i], typing))
    .filter(e => e)
    .join("\n\n");
  fs.writeFileSync("PrismicApi.ts", header + typings);
};
 
main();

Running this with node generate-prismic-types.js will generate the following file (PrismicApi.ts):

/*
 * This file is generated by generate-prismic-types.js. Do not edit by hand.
 *
 * This file contains types generated by querying the Prismic API.
 */
 
import { useEffect, useState } from "react";
import Prismic from "prismic-javascript";
 
export const prismic = Prismic.client(
  "https://my-project.cdn.prismic.io/api/v2"
);
 
export const usePrismicDocByType = <T,>(
  documentType: string,
  defaultValue: T
): T => {
  const [strings, setStrings] = useState<T>(defaultValue);
  useEffect((): void => {
    const fetchData = async () => {
      const resp = await prismic.getSingle(documentType, {});
      if (resp) {
        setStrings(resp.data);
      }
    };
    fetchData();
  }, [documentType]);
  return strings;
};
 
export interface MyPageStrings {
  header: string;
  body: string;
  footer: string;
}
 
const emptyMyPageStrings: MyPageStrings = {
  header: "",
  body: "",
  footer: "",
};
 
export const useMyPageStrings = (): MyPageStrings =>
  usePrismicDocByType<MyPageStrings>("my_page", emptyMyPageStrings);

The generated file can be regenerated as part of your dev build and checked in, or you can let Github Actions do it for you.

We can then get rid of all the boilerplate code and use the custom hook like this:

import React from "react";
import { useMyPageStrings } from "./PrismicApi";
 
function App() {
  const strings = useMyPageStrings();
  return (
    <div>
      <div>{strings.header}</div>
      <div>{strings.body}</div>
      <div>{strings.footer}</div>
    </div>
  );
}
 
export default App;

With all the benefits of type safety, as you'd expect.


Now, we’ll take a closer look at the generation code, so you can see how it works and check if any analogues exist in your API of choice.

We first query the top level Prismic API (topLevelQuery), to get the ID of the ref, and a list of all the types that we have in Prismic.

// console.log of the refs variable in the generation script
[{ id: "master", ref: "abcd1234", label: "Master", isMasterRef: true }];
// console.log of the types variable in the generation script
{ my_page: 'My Page',
  my_other_page: 'My Other Page' }

Then, for each type, we query Prismic (typeQuery) to see what fields it has:

// console.log of the typeInfo variable in the generation script
{ page: 1,
  results_per_page: 20,
  results_size: 1,
  total_results_size: 1,
  total_pages: 1,
  next_page: null,
  prev_page: null,
  results:
   [ { id: '12345',
       uid: null,
       type: 'my_page',
       href:
        'https://my-project.cdn.prismic.io/api/v2/documents/search?ref=abcd1234q=...',
       tags: [],
       first_publication_date: '2020-05-23T08:43:34+0000',
       last_publication_date: '2020-05-23T08:43:34+0000',
       slugs: [ 'header' ],
       linked_documents: [],
       lang: 'en-us',
       alternate_languages: [],
       data:
        { header: 'Header',
          body: 'This is a body.',
          footer: 'Footer' } } ],
  version: '8991d00',
  license: 'All Rights Reserved' }

The data we want is in .results[0].data. We only care for the keys of the data object, since the values are free to change at runtime. Once we’ve gotten the fields for each type (header, body, and footer in the case of the my_page type), we can generate the interfaces, including an object representing the default/empty value of the interface.


Removing fields from Prismic works as you’d expect. Deployed apps will simply fall back to empty strings for those fields during runtime, while any other codebases in development will regenerate the updated types and fix the code accordingly.

Credits to Philippe for enlightening me on this technique!