Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

overwrite field data with computedFields values #398

Open
nickfrosty opened this issue Mar 17, 2023 · 2 comments
Open

overwrite field data with computedFields values #398

nickfrosty opened this issue Mar 17, 2023 · 2 comments

Comments

@nickfrosty
Copy link

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
Copy link

melanieseltzer commented Mar 24, 2023

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
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants