saiichihashimoto / sanity-typed-schema-builder

Build Sanity schemas declaratively and get typescript types of schema values for free!
https://saiichihashimoto.github.io/sanity-typed-schema-builder
MIT License
67 stars 5 forks source link

Build Helper for inferring field types #196

Closed bradymwilliams closed 1 year ago

bradymwilliams commented 1 year ago

I have a few places in my codebase where I'm defining shared fields in an array to map over and alter in the document definitions. When I reference those shared fields in the document definition I get the following type error:

`Type '{ name: string; type: SanityType<{ options: (Omit<StringOptions | undefined, "list"> & { list?: [string | { title: string; value: string; }, ...(string | { title: string; value: string; })[]] | undefined; }) | undefined; ... 6 more ...; placeholder?: string | undefined; }, string, string, string>; }[]' is not assignable to type '[FieldOptions<string, ZodTypeAny, unknown, boolean>, ...FieldOptions<string, ZodTypeAny, unknown, boolean>[]]'.

Source provides no match for required element at position 0 in target.`

Weird thing is if I access a shared field by index on the array no typeerror shows so its only iteration that makes this throw. I even tried exporting the FieldsArray type from but that type requires arguments I'm not sure how to make dynamic. Any help/feedback appreciated.

const shared_fields = [
  {
    name: 'name',
    type: s.string(),
  },
];

// type error
const bar = s.document({
  name: 'bar',
  title: 'bar',
  fields: shared_fields,
});

// no type error
const foo = s.document({
  name: 'foo',
  title: 'foo',
  fields: [shared_fields[0]],
});
saiichihashimoto commented 1 year ago

I'll shoot from the hip here about what I think is going on. When it's inline in the s.document, it's typing things as literals more often than not, so the generics can pull out the actual fields. fields isn't just an array of arbitrary fields, it's an array with name: 'foo' with type: 'string', etc.

When you pulled it out into shared_fields, I'm assuming it's not of type { name: 'name', ...}[] but actually { name: string, ... }[]. When s.document gets that, it's unclear what to do. It'll be an object where the fields' names are strings, not specific strings. How would it know what the fields' names are or of what type? Try throwing in some as const on things that could get "generalized". For example, the name: 'name' as const. Does that change things?

If that's the issue, it would be nice to have some kind of exported type from this library you could use to make this a little easier. Until then, make sure the types of your shared_fields don't end up broader than is helpful. The moment it's something along the lines of { name: string; type: any }[], you're not actually restricting anything.

I haven't checked out your issue at all to see if this is the case.

bradymwilliams commented 1 year ago

Thanks for response 🙏 as const doesn't seem to affect anything. I'm already over my skis some here but something worth mentioning is I first reached for an s.field for the shared_fields items similar to sanity's defineField() experience.

saiichihashimoto commented 1 year ago

Yeah, so s.document uses generics with fields and those generics get auto-magically derived to a narrower type when you throw fields directly in, so it can give you a document with the correct shape. When it's in an external array like that, the array becomes a much broader type so, even if it didn't error, wouldn't give you the shape you wanted anyways.

Something like this would work, just so it ends up inferring the same type that document would:

const sharedFields = <
  Names extends string,
  Zods extends z.ZodTypeAny,
  ResolvedValues,
  Optionals extends boolean,
  FieldsArray extends TupleOfLength<
    FieldOptions<Names, Zods, ResolvedValues, Optionals>,
    1
  >
>(
  fields: FieldsArray
) => fields;

const fields = sharedFields([
  {
    name: "foo",
    type: boolean(),
  },
  {
    name: "bar",
    optional: true,
    type: string(),
  },
]);

const type = document({
  name: "foo",
  fields,
});

Something I could export from the package, just haven't yet.