0

I'm working on an Astro + TypeScript project where I have few data collections with specific schemas.

I'm using the Zod library to define and structure each data collection, which in a simplified way looks something like this:

import { defineCollection, z } from 'astro:content';

export const authorsCollection = defineCollection({
  schema: z.object({
    name: z.string(),
  }),
});

export const blogCollection = defineCollection({
  schema: z.object({
    title: z.string(),
    status: z.enum(['publish', 'trash', 'draft']).default('draft'),
  }),
});

export const collections = {
  authors: authorsCollection,
  blog: blogCollection,
};

I'm trying to write a helper function to handle collections differently based on their ID (e.g. be able to filter entries based on their status field in the blog posts while not having it in the authors collection).

Here is the function I'm using:

import { getCollection } from 'astro:content';

import { collections as c } from '@content/config';

const collections = Object.keys (c);
type CollectionType = keyof (typeof c); // This solely translates to "authors" | "blog" string literal type.

type CollectionSettings = {
  status?: string[]; // Just for illustrative purposes.
};

export async function retrieveCollection (id: CollectionType, preferences = <CollectionSettings>{}) {
  const settings = Object.assign ({
    status: ['publish'], // Set default values.
  }, preferences);

  let entries = await getCollection (id); // Retrieve an initial list of content entries by its id.

  if (id === 'blog') { // This is where I distinguish whether or not the current request to the function belongs to a certain collection but the compiler doesn't seem to recognize it.
    entries = entries.filter (({ data: { status }}) => settings.status.includes (status)); // Here lies the error and the main reason for my current question.
  }

  // Perform another procedures such as filtering, searching fields, sorting entries by date, etc.

  return entries;
};

Functionally I have no complaints, as everything works as it should; however, the compiler keeps complaining about Property 'status' does not exist on 'authorsCollection' type and I would like to be able to fix it without resorting to @ts-ignore directive.

The problem seems to be that TypeScript does not recognize the specific type of the collection within the conditional block. For example, within the if (id === 'blog') block, TypeScript does not correctly infer that entries conforms to the BlogPost schema, leading to an error when accessing the status property (which obviously does not exist).

I have previously read the questions related to TypeScript type narrowing on this site, checked with ChatGPT and GitHub Copilot without results.

So my questions are:

  1. How can I improve the type discrimination so that TypeScript recognizes the specific collection schema based on the string literal type?
  2. Is there a better way to handle these types to avoid such type inference issues?

I would appreciate any suggestions on how to improve my type structure and function logic to ensure TypeScript correctly handles the specific schemas for each collection.

Thank you all in advance.

2 Answers 2

0

In the end got desperated and since I couldn't find a satisfactory solution I decided to simply force a cast to CollectionEntry like this:

import type { CollectionEntry } from 'astro:content';

export async function retrieveCollection (id: CollectionType, preferences = <CollectionSettings>{}) {
  type EntryType = CollectionEntry <typeof id>;

  // The rest of the code remained the same as the code example above.

  if (id === 'blog') {
    entries = entries.filter (({ data }: EntryType) => {}); // Type casting here.
  }
}

And while this is somewhat seems to keep the compiler happy, I'm still open to learn a better solution.

0

You need to help TypeScript narrowing the type. Try:

const entries = id === 'blog'
  ? (await getCollection(id)).filter(blogEntry => settings.status.includes(blogEntry.data.status));
  : await getCollection(id);
3
  • I tried a similar approach at first, but in my case I have multiple different collections that, although they share many fields in common, some require additional treatment (like field filtering, key search, sorting entries by date) according to specific properties. And after calling getCollection(id) separately (as you suggest) I started repeating quite a bit of code to the point that it was probably better to just create a unique function for each collection. Tyvm for taking the time to help. Commented Jul 3 at 14:16
  • okay, but if it answers your original question, you should accept it anyway...
    – mb21
    Commented Jul 3 at 15:04
  • The thing is that your solution doesn't scale well after adding many collections. As I said before, I gave a simplified example, and managing multiple collections of content by nesting conditions within each other when they share so many fields in common should not be the best approach. Thank you anyway. Commented Jul 20 at 17:10

Not the answer you're looking for? Browse other questions tagged or ask your own question.