Is this approach to fix a typing error between typescript and Contentful's content delivery API safe?

298 Views Asked by At

I'm using Astro and Contentful to make a blog. I have a content type called author which has 3 fields.

Name Field Type
Full Name Short text
Headshot Media
Bio Rich Text

I'm working on creating a component that will list out all the authors. My code looks as follows:

---
import { contentfulClient } from "../lib/contentful";
import { documentToHtmlString } from "@contentful/rich-text-html-renderer";
import type { Asset, EntryFieldTypes } from "contentful";


interface Author {
    contentTypeId: "author",
    fields: {
        fullName: EntryFieldTypes.Text,
        headshot: EntryFieldTypes.AssetLink,
        bio: EntryFieldTypes.RichText
    }
}

const authors = await contentfulClient.getEntries<Author>({
    content_type: "author"
})

---
<h1>Authors</h1>
{
    authors.items.map((author) => (
        <p>{author.fields.fullName}</p>
        <img src={`https:${(author.fields.headshot as any).fields.file.url}`} alt="" srcset="">
    ))
}

This works but I had to cast author.fields.headshot as any because typescript was complaining that fields wasn't a property of headshot. And my intellisense could only find sys as a property of headshot.

So this:

<img src={`https:${author.fields.headshot.fields.file.url}`} class="w-full max-w-48" alt="" srcset="">

Does work locally. But when I go to deploy to Netlify the build fails since typescript won't compile. My solution works but it feels dangerous. I thought maybe I was using the wrong EntryFieldTypes property in my Author interface but AssetLink seems to be the most appropriate.

3

There are 3 best solutions below

3
Robban On

I believe your headshot field should be of type Asset not AssetLink which is a link within a rich text structure.

headshot: Asset,
1
devio On

When you define headshot as an EntryFieldTypes.AssetLink, TypeScript expects a specific structure for that field, which does not include the nested fields property directly.

To resolve this, you need to properly type the structure of the AssetLink and its nested properties. The AssetLink should have a sys property and potentially other properties depending on how Contentful structures it.

Here's how you can adjust your types:

interface Author {
    contentTypeId: "author",
    fields: {
        fullName: EntryFieldTypes.Text,
        headshot: Asset,
        bio: EntryFieldTypes.RichText
    }
}

// Define the structure of an Asset based on Contentful's data model
interface Asset {
  sys: { id: string };
  fields: { 
    file: { 
      url: string;
      details?: any; // Include any additional expected properties here
      fileName?: string;
      contentType?: string;
    }; 
  };
}

By defining an Asset interface that matches what Contentful returns, you help TypeScript understand the shape of your data. This way, when you access author.fields.headshot.fields.file.url, TypeScript knows what to expect.

THe component code should also be adjusted:

<h1>Authors</h1>
{
    authors.items.map((author) => (
        <>
            <p>{author.fields.fullName}</p>
            {/* Make sure to check if headshot and file are defined before accessing url */}
            <img src={author.fields.headshot && author.fields.headshot.fields.file.url ? `https:${author.fields.headshot.fields.file.url}` : ''} alt="" class="w-full max-w-48" />
        </>
    ))
}

Each element is wrapped in a fragment (<>...</>) because .map() must return a single element. Also, I added checks before accessing nested properties like .file.url. This will prevent runtime errors if some authors don't have a headshot assigned.

Let me know if this deploys or not...

0
mb21 On

The Contentful TypeScript definitions are really quite something. Smells heavily like they had to tack them on a dynamic js lib after the fact.

I had a look at their tutorial which mentions EntryFieldTypes.Asset, which doesn't exist. But the closest I got is:

import { type Asset, type EntryFieldTypes } from 'contentful'

const headshot: Asset;

(author.fields.headshot as Asset).fields.file?.url

Note the as Asset is still needed, because the Entry type for some reason sets the type of the field headshot to never. (The author variable is not of type Author but of type Entry<Author, undefined, string>.)

Finally, you could try generating the types automatically.