contentlayerdev / contentlayer

Contentlayer turns your content into data - making it super easy to import MD(X) and CMS content in your app
https://www.contentlayer.dev
MIT License
3.3k stars 202 forks source link

overwrite `field` data with `computedFields` values #398

Open nickfrosty opened 1 year ago

nickfrosty commented 1 year ago

When defining a document with a field and computedFields child item with the same name, the computedFields field seems to be appended onto the generated type. Resulting in undesired types for the field.

I would have expected the computedFields data to overwrite the field data, both the description and the type.

For reference, I am converting a comma separated string of tags into a parsed array via the computedFields. Here is an example of my document definition:

export const Blog = defineDocumentType(() => ({
  name: "Blog",
  filePathPattern: `blog/**/*.md`,
  fields: {
    tags: {
      type: "string",
      description: "Comma separated listing of tags",
      required: false,
    },
  },
  computedFields: {
    tags: {
      description: "new description",
      type: "list",
      of: { type: "string" },
      resolve: (item) =>
        item?.tags?.split(",")?.map((tag) => tag.trim()) ?? undefined,
    },
  },
}));

Generates these types (simplified for the example):

/** Document types */
export type Blog = {
  /** Comma separated listing of tags */
  tags?: string | undefined
  /** new description */
  tags: list
}  

Extra note: It also seems like the computedFields children do not support list types, or it may be a bug?

melanieseltzer commented 1 year ago

I ran into this also. In my case, I want to define tags like this my mdx blog posts:

tags:
  - React
  - Next.js
  - Something Else

... and transform to an array of objects with the slug work already done, ready for consumption by component(s):

tags: [
  { displayName: 'React', slug: 'react' },
  { displayName: 'Next.js', slug: 'nextjs' },
  { displayName: 'Something Else', slug: 'something-else' },
]

I ended up working around the typing issues by omitting tags from the original type and re-typing it myself, then using that custom type everywhere.

Note - I acknowledge my solution might be a little cumbersome for others who are importing from contentlayer/generated directly, but in my case I was already creating a custom BlogPost type anyway (I never import from contentlayer/generated directly in components/pages so everything is not coupled to contentlayer as a concept). So doing some omit work was fine for me.

// inside contentlayer.config.ts

export const BlogPost = defineDocumentType(() => ({
  name: "BlogPost",
  filePathPattern: `blog/**/*.mdx`,
  fields: {
    tags: { type: 'list', required: true, of: { type: 'string' } },
  },
  computedFields: {
    // This takes all the tags defined in the frontmatter (list of strings) and automatically
    // derives slugs for them (saving us having to do it each time we consume them).
    // This ultimately changes the type of the field from `string[]` (in the frontmatter)
    // to `Tag[]` which will be consumed everywhere.
    tags: {
      type: 'list',
      resolve: doc =>
        // For some reason, the actual value of `tags` here is PlainArr, so we have to map over the `_array` property instead.
        // I don't really care to figure out the typing for that :/
        // ref: https://github.com/contentlayerdev/contentlayer/issues/150

        /* eslint-disable*/
        // @ts-ignore
        doc.tags._array.map((tag: string) => ({
          displayName: tag,
          slug: kebabCase(tag),
        })),
      /* eslint-enable  */
    },
}));

Inside my custom src/content/blog/types.ts file (which I then import from whenever I need the BlogPost type):

import type { BlogPost as CLBlogPost } from 'contentlayer/generated';

export type Tag = {
  displayName: string;
  slug: string;
};

export type BlogPost = Omit<CLBlogPost, 'tags'> & {
  // `tags` has a conflicting type due to defining it in both `fields` and `computedFields`
  // in the schema, so we've omitted it here and forced it to the type we need.
  // ref: https://github.com/contentlayerdev/contentlayer/issues/398
  tags: Tag[];
};

Then I can create a base selector to use everywhere:

import { allBlogPosts } from 'contentlayer/generated';

import type { BlogPost } from '~/content/blog/types'; // the custom type I've defined above

export const getAllBlogPosts = () => allBlogPosts as unknown as BlogPost[];

Anytime I need to retrieve all the blog posts, I would use the custom getAllBlogPosts selector, so tags are properly typed as Tag[] throughout.

Hope it's useful for someone! Though it would be nice if the type wasn't appended so we didn't have to do this.

schickling commented 1 year ago

Thanks a lot for opening this issue. I agree with the underlying issue but want to be careful with introducing (potentially) breaking changes too quickly. I'll account for this problem in upcoming API iterations.

For now as a (unfortunately cumbersome) workaround you can give your computed tags field another name e.g. computedTags which shouldn't have the problem above.