Content relationship types within another content relationship in a Slice

Hello, Prismic People :grin:!

Some time ago, I encountered issues with Content Relationship types in a Slice. However, I managed to resolve them by following the solution provided by angelsashmore, which I found in the responses to the topic created by Joseph.

Topic:
:point_right: Types for content relationship in a Slice

And also in the documentation:
:point_right: Use TypeScript with Next.js

:man_technologist: Upon exploring the solution further, I managed to condense the code and arrive at the following:

import * as prismicH from "@prismicio/helpers";
import * as prismicT from "@prismicio/types";

const hasServiceData = <
  TContentRelationshipField extends prismicT.ContentRelationshipField
>(
  contentRelationshipField: TContentRelationshipField
): contentRelationshipField is TContentRelationshipField &
  Content.ServiceDocument => {
  return (
    prismicH.isFilled.contentRelationship(contentRelationshipField) &&
    typeof contentRelationshipField.data === "object" &&
    contentRelationshipField.data !== null &&
    contentRelationshipField.id !== null &&
    contentRelationshipField.uid !== null
  );
};

// Example of usage within a Slice function
hasServiceData(slice.items[0].service) &&
    console.log(
      slice.items[0].service.data.title,
      slice.items[0].service.id,
      slice.items[0].service.uid,
      slice.items[0].service.data.testimonials[0]?.author
    );

"If everything went well, then what's the problem :face_with_monocle:?"

The problem is that you don't have just one Content Relationship, but a Content Relationship (case_study) within another Content Relationship (service).

So the current solution works for all the other fields, but when it gets to the innermost Content Relationship, it stops there and doesn't do the typing.

I tried to change everything to see what could be done, but I only found two ways to solve this, the first would be to create another function just for this internal Content Relationship, and the second would be to do it more "by hand", that is, uid: uid , id: id and so on until you arrive.

The first solution is acceptable, but I would like to keep everything in just one function, and the second solution I didn't find very interesting, as it would be a more "manual" process, something that would get even worse the more and more fields were added.

:man_technologist: The code for the second solution would look something like this:

const hasServiceData = <
  TContentRelationshipField extends prismicT.ContentRelationshipField
>(
  contentRelationshipField: TContentRelationshipField
): contentRelationshipField is TContentRelationshipField & {
  id: Content.ServiceDocument["id"];
  uid: Content.ServiceDocument["uid"];
  data: {
    title: Content.ServiceDocumentData["title"];
    header_image: Content.ServiceDocumentData["header_image"];
    tag: Content.ServiceDocumentData["tag"];
    color: Content.ServiceDocumentData["color"];
    description: Content.ServiceDocumentData["description"];
    headline: Content.ServiceDocumentData["headline"];
    testimonials: Content.ServiceDocumentData["testimonials"];
    highlights: Content.ServiceDocumentData["highlights"];
    case_studies: { case_study: Content.CaseStudyDocument }[];
  };
} => {
  return (
    prismicH.isFilled.contentRelationship(contentRelationshipField) &&
    typeof contentRelationshipField.data === "object" &&
    contentRelationshipField.data !== null &&
    contentRelationshipField.id !== null &&
    contentRelationshipField.uid !== null
  );
};

So, do you guys have any ideas for a third solution that is more 'elegant'? Or do you think that the two proposals mentioned above are the only ways to infer types from a Content Relationship nested within another Content Relationship in a Slice?

Hi @gabrielv,

This is a fairly complex problem that currently doesn't have an official solution. However, I wrote a helper that I think may help in your case.

It still requires manually listing out which fields you want typed at compile time and checked at run time, but it should reduce the amount of code you need to write.

// src/lib/isFilledRelatedData.ts

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

type DocumentData<TDocumentType extends Content.AllDocumentTypes["type"]> =
  Extract<Content.AllDocumentTypes, { type: TDocumentType }>["data"];

export function isFilledRelatedData<
  TDocumentType extends Content.AllDocumentTypes["type"],
  TFieldID extends keyof DocumentData<TDocumentType>,
>(
  linkField: LinkField,
  documentType: TDocumentType,
  fieldID: TFieldID,
): linkField is FilledContentRelationshipField & {
  data: {
    [P in keyof DocumentData<TDocumentType> as P extends TFieldID
      ? P
      : never]: DocumentData<TDocumentType>[P];
  };
} {
  return (
    isFilled.contentRelationship(linkField) &&
    linkField.type === documentType &&
    typeof linkField.data === "object" &&
    linkField.data !== null &&
    fieldID in linkField.data
  );
}

You can use it like this:

if (
  isFilledRelatedData(slice.primary.button_link, "page", "meta_title")
) {
  slice.primary.button_link.data.meta_title
  // ^ Typed as Content.PageDocumentData["meta_title"]
}

Or if you need to check and type nested related data:

if (
  isFilledRelatedData(slice.primary.button_link, "page", "parent") &&
  isFilledRelatedData(slice.primary.button_link.data.parent, "page", "meta_title" )
) {
  slice.primary.button_link.data.parent.data.meta_title
  // ^ Typed as Content.PageDocumentData["meta_title"]
}

Using this pattern, you can write your own helper functions specific to your project. How you build that will depend on your content models.

In the future, we may provide a helper like this in @prismicio/client. For now, you can copy this code into your project.