Jesse McLean
Article

Improving Type-Safety for Prismic Data

Published
Reading time
4 minutes

/**
 * Type-safe wrapper around Prismic's `isFilled.contentRelationship` function to make link fields more type-safe.
 * You'll want to make sure that your Prismic client is set up to handle typed content.
 *
 * If using Next.js and Slice Machine, you can use the `@prismicio/client` package to set up typed content.
 * @see {@link https://prismic.io/docs/typescript-nextjs} for more information.
 *
 * Prismic also offers support via the `prismic-ts-codegen` package.
 * @see {@link https://prismic.io/docs/technical-reference/prismic-ts-codegen} for more information.
 */

import {
  type Content,
  type FilledContentRelationshipField,
  isFilled,
  type LinkField,
} from "@prismicio/client";

// Extracts the data of a specific document type from Prismic content
type DocumentData<TDocumentType extends Content.AllDocumentTypes["type"]> =
  Extract<Content.AllDocumentTypes, { type: TDocumentType }>["data"];

/**
 * Checks if a link field in Prismic is a filled content relationship field and contains the specified field IDs.
 * This is useful for checking if a link field is a valid link to a specific document type and contains the specified field IDs.
 * This acts as a wrapper around the `isFilled.contentRelationship` function from Prismic to make link fields more type-safe.
 *
 * @template TDocumentType - The type of the document.
 * @template TFieldID - The field IDs to check within the document data.
 * @param {LinkField} linkField - The link field to check.
 * @param {TDocumentType} documentType - The type of the document to match against.
 * @param {TFieldID[]} fieldIDs - The field IDs to check within the document data.
 * @returns {linkField is FilledContentRelationshipField & { data: { [P in keyof DocumentData<TDocumentType> as P extends TFieldID ? P : never]: DocumentData<TDocumentType>[P]; } }} - Returns true if the link field is a filled content relationship field and contains the specified field IDs.
 *
 * @example
 * ```ts
 * import { isFilledLinkedContent } from "./is-filled-linked-content";
 *
 * const isValidLink = isFilledLinkedContent(linkField, "article", ["title"]);
 * ```
 */
export function isFilledLinkedContent<
  TDocumentType extends Content.AllDocumentTypes["type"],
  TFieldID extends keyof DocumentData<TDocumentType>,
>(
  linkField: LinkField,
  documentType: TDocumentType,
  fieldIDs: TFieldID[],
): linkField is FilledContentRelationshipField & {
  data: {
    [P in keyof DocumentData<TDocumentType> as P extends TFieldID
      ? P
      : never]: DocumentData<TDocumentType>[P];
  };
} {
  return (
    // Check if the link field is a filled content relationship field
    isFilled.contentRelationship(linkField) &&
    // Check if the link field's type matches the specified document type
    linkField.type === documentType &&
    // Ensure the link field's data is an object and not null
    typeof linkField.data === "object" &&
    linkField.data !== null &&
    // Check if all specified field IDs exist in the link field's data
    fieldIDs.every(
      (fieldID) => fieldID in (linkField.data as Record<string, unknown>),
    )
  );
}