Types for content relationship in a Slice

Hello, I am wondering if we could get an example on how to add types for a content relationship that comes from a slice? The docs have an example for a content relationship field on the custom type itself using fetchlinks, but I am having trouble trying to find out how to add types to a content relationship that comes from a slice while still using the fetchlinks option in getStaticProps.

For example when I loop through slice.items my blog_post has the type of EmptyLinkField<"Document"> | FilledContentRelationshipField<string, string, unknown> as generated by Slice Machine.

So using fetchlinks I am able to display each blog post's title using item.blog_post.data.title in a PrismicRichText component but I get Property 'data' does not exist on type 'EmptyLinkField<"Document"> | FilledContentRelationshipField<string, string, unknown>'. Property 'data' does not exist on type 'EmptyLinkField<"Document">'..

Any leads or insight would be greatly appreciated!

Hi Joseph,

Typing a deeply nested Content Relationship field with a data property is challenging if you do it from the top-level client query, which is shown in the documentation's example (see here).

If you need to type a Slice's Content Relationship field, I recommend doing runtime checking with type predicates. This saves you from performing complicated TypeScript gymnastics to override the Slice's types, which can contain many branches (think: different Slice types, each with their own variations, and each with primary and items fields).

A Content Relationship's data field is typed as unknown by default, which works well with type predicates.

The following example checks that a Content Relationship field contains a data property with a parent field. Remember that the data property is only filled if the top-level query included a fetchLinks or graphQuery option, which may not always be the case.

import { Content } from "@prismicio/client";
import { PrismicText, SliceComponentProps } from "@prismicio/react";
import * as prismicH from "@prismicio/helpers";
import * as prismicT from "@prismicio/types";

const hasParentData = <
  TContentRelationshipField extends prismicT.ContentRelationshipField
>(
  contentRelationshipField: TContentRelationshipField
): contentRelationshipField is TContentRelationshipField & {
  data: {
    parent: Pick<Content.PageDocument["data"], "title">;
  };
} => {
  return (
    prismicH.isFilled.contentRelationship(contentRelationshipField) &&
    typeof contentRelationshipField.data === "object" &&
    contentRelationshipField.data !== null &&
    "parent" in contentRelationshipField.data
  );
};

export default function Image({
  slice,
}: SliceComponentProps<Content.ImageSlice>) {
  return (
    <div>
      {hasParentData(slice.primary.link) && (
        <PrismicText field={slice.primary.link.data.parent.title} />
      )}
    </div>
  );
}

The hasParentData function checks that the provided field contains a data property, is an object, and contains a parent field. It types the data property by adding a data.parent property to the provided field (see the is keyword in the function's return type).

If you check this code in your editor, you'll see that slice.primary.link.data.parent.title is typed as PageDocument['data']['title'] (in my case, that is a Rich Text field). You will need to adjust the type names according to your project.

This pattern can be used across your app whenever you need to check if a Content Relationship field has linked fields.

If you have any questions, let me know! :slight_smile:

Ok @angeloashmore ,
Update Below :point_down:
You were so kind as to say if I had any questions to let you know. This topic has sunk my TypeScript "battleship." I have a PageType called Brand. In a CallToAction slice, I have a ContentRelationship field inside of the repeatable zone. I want to access the Brand's data but I'm getting the typescript errors that data does not exist on the type.

Property 'data' does not exist on type 'ContentRelationshipField<"brand">

I think your explanation must be what I need, but it just doesn't make sense to me. I tried to follow the documentation for this (even though it shows GetStaticProps).

I can't help but wonder if the approach to this has changed with regard to the app router. Allow me to provide some more details so that you might be able to help me.

On the [uid] > page.tsx file I have the following to fetch data:

const page = await client
    .getByUID<
      Content.PageDocument & {
        data: {
          brand: {
            data: Pick<
              Content.BrandDocument['data'],
              'title' | 'description' | 'logo'
            >
          }
        }
      }
    >('page', params.uid, {
      fetchLinks: ['brand.description', 'brand.logo', 'brand.title'],
    })
    .catch(() => notFound())

Here's my Update:
I "slept on it," and this is what seems to be working for me. I tried your hasParentData, but then realized that I don't have a deeply nested situation. I then took your recommendation to look at predicates. I'm not sure that I've done it correctly, but it's working here's what I did for the brand example mentioned above:

const isBrand = (brand: object): brand is BrandDocument => {
  return (brand as BrandDocument).data !== undefined
}

isBrand(item.brand) ? (
  <PrismicRichText
  field={item.brand.data.title}
    components={{
    heading2: ({ children }) => (
    <Heading
        as="h2"
        size="3xl"
        className="my-2 text-skin-neutral lg:my-3"
     >
     {children}
     </Heading>
      ),
     }}
     />
     ) : (
          <Heading as="h2" size="3xl">
           Add Heading or Product Title
           </Heading>
         )

Hi @nf_mastroianni, a similar question came up in this thread:

Check out my reply there to see how you could possibly simplify your isBrand function into something more flexible.

You should be able to use the isFilledRelatedData() helper like this:

if(
    isFilledRelatedData(item.brand, "brand", "title") &&
    isFilledRelatedData(item.brand, "brand", "description") &&
    isFilledRelatedData(item.brand, "brand", "logo"
) {
    // ...
}

With that, you can remove the <...> code in your getByUID() call:

const page = await client
    .getByUID('page', params.uid, {
      fetchLinks: ['brand.description', 'brand.logo', 'brand.title'],
    })
    .catch(() => notFound())
1 Like

Thank you for your reply. Do you think a utility function like this might serve enough of a purpose for Prismic users that it might end up in the @prismicio/client library? I use isFilled like it's my job, so I'd love me a isFilledRelatedData helper. :wink:

@nf_mastroianni It's possible we could add something like it to @prismicio/client!

The function's name can be misleading since it doesn't actually check if the field is filled. Instead, it checks that the linked field was queried via fetchLinks/graphQuery.

We would probably refocus the helper into something like hasRelationshipField(), which would still require you to check that the field was filled with isFilled().

if (
    hasRelationshipField(item.brand, "brand", "title") &&
    isFilled(item.brand)
) {
    // ...
}

I'll open an issue on GitHub so we can track the request. :slight_smile:

1 Like

Opened the issue here: A helper to check if a content relationship's field was queried · Issue #330 · prismicio/prismic-client · GitHub

1 Like