#Multiple collections with same schema

18 messages · Page 1 of 1 (latest)

rancid parrot
#

I'd like to organize my content into multiple directories, which from what I've read means I need multiple collections, but they would all have the same schema. There will be a few more fields in the future, but basically my config looks like this:

const basicPageConfig = {
  schema: z.object({
    title: z.string(),
  }),
};

export const basicCollections = {
  foundation: defineCollection(basicPageConfig),
  guidelines: defineCollection(basicPageConfig),
  // ...more 'basic page' collections
} as const;
export type BasicCollection = keyof typeof basicCollections;

export const collections = {
  blog,
  ...basicCollections,
};

I want to be able to use the same page layout component for all these "basic pages", and I want it to be typed. However, every time I try to call getCollection with a union type from these collection keys, the type returned by that function is never[].

// `collection` is type `BasicCollection`, and
// BasicCollection = "foundation" | "guidelines"
// But `posts` is `never[]`!
const posts = await getCollection(collection);

My conclusion is that the generic getCollection doesn't support union types. So how do I do what I'm doing? Is it possible to make them all part of the same collection but place them in different subdirectories in my content directory with matching paths in their URLs?

oak schooner
#

I think you can do something like this ts const posts: CollectionEntry<'foundation' | 'guidelines'>[] = await getCollection(collection); Or you can make sperate calls to each collection like this: ```jsx
const foundation = await getCollection('foundation');
const guidelines = await getCollection('guidelines');
...

It is possible to nest them as sub folders inside a single collection although it would be harder to get entries of a specific type and they would always share the same schema so it is not feature/future proof, I would not recommend it
rancid parrot
#

@oak schooner , unfortunately with const posts: CollectionEntry<BasicCollection>[] = await getCollection(collection);, TypeScript still insists that posts is never[].

async function getCollectionPaths(collection: BasicCollection) {
  const posts: CollectionEntry<BasicCollection>[] = await getCollection(collection);
  return posts.map((post) => ({
    params: {
      collection,
      slug: post.slug,
    },
    props: post,
  }));
}

results in

src/pages/[collection]/[...slug].astro:12:20 Error: Property 'slug' does not exist on type 'never'.
11          collection,
12          slug: post.slug,
                       ~~~~
13        },
#

I will work on getting a demo ready

oak schooner
rancid parrot
#

It's in a monorepo, and we use yarn, but as long as you match node versions, it shouldn't make a big difference if you use npm. yarn install in the root and then the astro project is in packages/docs-astro

#

oh, yarn install && yarn build in the root then astro should work

oak schooner
#

I think I got this working properly, I made a type like this: ```ts
export type AnyEntry = {
[K in BasicCollection]: CollectionEntry<K>;
}[BasicCollection];

then assigned it like this ```ts
const posts = (await getCollection(collection) as unknown as AnyEntry[]);
rancid parrot
#

Thank you for looking into this, @oak schooner . Unfortunately AnyEntry seems to evaluate to any, which makes posts any[], so we actually lose the type safety on it

oak schooner
#

🤔 That is weird, It seems to be working on my end:

oak schooner
#

I also got this working with a type like this: ```ts
export type AnyEntry<T = BasicCollection> = T extends BasicCollection
? CollectionEntry<T>
: never

rancid parrot
#

I wonder if it's editor TS vs project TS

rancid parrot
#

I've tried it both in my editor version (5.1.3) and project version (4.9.5), and they both evaluate the return value to any[] even when type-casting to AnyEntry[]. It doesn't make any sense.

rancid parrot
#

It works! I wasn't actually importing CollectionEntry in config.ts where I had defined your AnyEntry type, but somehow the type checks (yarn astro check) weren't catching the missing import!

#

and it works without using as unknown

rancid parrot
#

As I think about how the collection types on the site will expand and play around with your AnyEntry type, I'm observing that it's a better type than the provided CollectionEntry type, and I wonder if the Astro types could be improved. Consider the following definitions:

const basicPageConfig = {
  schema: z.object({
    title: z.string(),
  }),
};

export const basicCollections = {
  foundation: defineCollection(basicPageConfig),
  guidelines: defineCollection(basicPageConfig),
  bob: defineCollection({
    schema: z.object({
      title: z.string(),
      anotherProp: z.string(),
    }),
  }),
} as const;
export type BasicCollection = keyof typeof basicCollections;
export type BasicEntry<T = BasicCollection> = T extends BasicCollection
  ? CollectionEntry<T>
  : never;

export const collections = {
  ...basicCollections,
};

In these new definitions, I added a bob collection that has a title like the other ones, but it adds one anotherProp property.

I've found that when going through pages defined by

const pages = (await getCollection(collection)) as BasicEntry[];

TypeScript will pick up on the fact that each page has a data.title, and it does not include the data.anotherProp. It seems to be finding the common properties and keeping those. It's not technically a union type, because it does not include the differences. That's useful in a template that renders links for bunch of pages that all have a title but may not have other things in common.

What if this is just the way the CollectionEntry type worked? If the items in an array of CollectionEntry<'a' | 'b'> were the common pieces of CollectionEntry<'a'> and CollectionEntry<'b'>? Seems like we get that for free with your definition above, but I could be vastly over-simplifying. I also don't understand how it works.