Discussion about merging changes to @sanity-typed/types into Sanity.io and improving dev story for custom types.

18 replies
Last updated: Aug 9, 2023
Hey! I built
@sanity-typed/types
(link ) in an effort to get proper sanity types out of
defineType
,
defineField
, and
defineArrayMember
. I'd like to merge my changes as much as possible into sanity itself. My question is, would I just start making PRs to the repo? To get some of these changes in, I'd have to make some big changes and I'm hoping to get some confirmation that this is the direction sanity wants to go before I invest the time to do that.
AI Update

That's awesome that you built @sanity-typed/types! Contributing improved TypeScript types to Sanity would be a great addition to the project. Here's how to approach this:

Start with the Contributing Guidelines

Before diving into PRs, definitely check out the CONTRIBUTING.md in the Sanity repo. The key points are:

  • Open a discussion first for significant changes - Before investing time in big refactors, it's best to get buy-in from the team
  • Pull requests should be made against the main branch
  • The Sanity team prefers squash + merge for most PRs
  • Make sure changes are tested and production-ready before submitting

Since you mentioned needing to make "big changes" to get your improvements into Sanity, here's what I'd suggest:

  1. Open a GitHub Discussion or Issue first - Head to the Sanity GitHub repo and either:

    • Start a Discussion to propose your TypeScript improvements
    • Open an Issue describing what you'd like to improve and why

    Explain what improvements your library provides and how they'd benefit the core library. Link to your @sanity-typed/types repo so they can see what you've built.

  2. Join the Sanity Community Slack - The Sanity Community is very active and helpful. You can connect with core maintainers there to discuss your proposal informally before investing significant time.

  3. Start small if possible - If you can break your changes into smaller, incremental PRs rather than one massive change, that's usually easier to review and merge. Even if the end goal requires big changes, see if there's a path to get there incrementally.

  4. Reference existing patterns - Look at recent TypeScript-related PRs in the repo to understand their code style and testing expectations.

Why This Matters

Getting alignment upfront is crucial because:

  • The team may have specific architectural reasons for current type implementations
  • They might be working on similar improvements already
  • There could be breaking change considerations for the API
  • They'll want to ensure your approach fits their long-term vision

The Sanity team is generally very open to community contributions, but for architectural changes to core types, they'll definitely want to discuss the approach before you invest weeks of work. Starting the conversation now will save you time and increase the chances of your work being merged!

// From sanity's docs: <https://www.sanity.io/docs/schema-field-types#e5642a3e8506>
// import { defineField, defineType } from "sanity";
import { defineField, defineType } from "@sanity-typed/types";
// This is where the magic happens
import { InferValue } from "@sanity-typed/types";

// Import this into sanity's createSchema, as usual.
export const product = defineType({
  name: "product",
  type: "document",
  title: "Product",
  fields: [
    defineField({
      name: "productName",
      type: "string",
      title: "Product name",
    }),
    defineField({
      name: "tags",
      type: "array",
      title: "Tags for item",
      of: [
        defineArrayMember({
          type: "object",
          name: "tag",
          fields: [
            { type: "string", name: "label" },
            { type: "string", name: "value" },
          ],
        }),
      ],
    }),
  ],
});

// Import this into your application!
export type Product = InferValue<typeof product>;

/**
 *  Product === {
 *    _createdAt: string;
 *    _id: string;
 *    _rev: string;
 *    _type: string;
 *    _updatedAt: string;
 *    productName?: string;
 *    tags?: ({
 *      label?: string;
 *      value: string;
 *    })[];
 *  };
 **/

Cool solution! I’ve forwarded this to the team to look at 🙇‍♂️
Hey, very cool. What is the dev story for working with custom types (alias/global/user defined types)? Ie how is "mytype" inferred?
user Q
I have one of a few ideas for this:1.
InferValue<typeof schema>
becomes
InferValue<typeof schema, { [type]: typeof customSchema }>
and it can bring in those custom schemas where they're used.2. Create a
MakeInferValue
, so there's the usual
InferValue<typeof schema>
, but then you can build your own
type MyInferValue<Value> = MakeInferValue<Value, { [type]: typeof customSchema }>
.3. Somehow, infer the values directly off of sanity's
createSchema
instead of using
InferValue
per schema. It'll have all of the
defineType
, so getting the custom types might be more feasible that way.4. Create a global
SanityCustomTypes
that
InferValue
uses and it's the user's job to augment with their new types.Ultimately, the `defineType`s from the "root" level have to somehow make their way back into where those custom types are referenced, so it can get weird (especially if things become cyclical). Would love some input.
Manually registrering custom types in intrinsic types lookup interface will work using module augmentation and interface merging, but I fear it will require a lot of boilerplate.
As you say, the cyclic nature of this makes it though. When we looked at this previously the dx story for this bit seems hard to get right.
Can you explain why custom types exist? It seems like it's for object reuse but, since all the schemas are just JS, it would be easier to just reuse the same object somewhere.
Instead of:

{ name: 'customName', type: 'object', fields: [...] }

{ name: 'somewhereElse, type: 'object', fields: [..., { name: 'foo', type: 'customName' }] }
Couldn't we do:

const customName = { name: 'customName', type: 'object', fields: [...] };

{ name: 'somewhereElse, type: 'object', fields: [..., customName] }
It feels a lot like reimplementing JS variables, but I'm assuming there's more to it.
But to my original question: I'd love to figure out what pieces of this can make its way into sanity itself. My goal is for it to all get in there but I'm imagining it'll happen in pieces. For example, to infer objects properly, a field's typescript definition needs to carry whether or not it's required, so the object can know whether it's
{ key?: value }
or
{ key: value }
. Should I just go for it with PRs against sanity for things like this?
Uhhh... idk if this is because an updated version of typescript did some magic, but this POC I wrote up for custom AND cyclic types worked immediately. It's unfortunate to have a breaking change for a library I released yesterday, but I think this solves all use cases right away.
Wow if this actually works, thats awesome. Are you sure that defining Foo and Bar here is not the reason the "cycle is broken"?
Keep in mind that plugins will complicate this, since they add to available types. But those could possible register using interface merging.

On named types:
You need them for type reuse. Code, color (plugins) for instance, and it is very common to have reusable types for portable text fields ect.
We encourage hoisting types like this, and is an actual requirement for our GraphQL api.

Im optimistic about where you are heading with this, but the devil is in the details if the full api surface
😅
I'll have to test it out, but it's looking promising. Even if the definitions are what do it, we can do this in two steps: one, that infers that intermediate step, and another that expands it all. If we want, we can have another generic that also expands
reference
into the document it references, for convenience.
I was imagining that plugin types could fit into the second parameter of that
ExpandNamedTypes<TypeToExpand, NamedTypes | PluginTypes | Etc>
. I'm imagining there's a DX that makes sense here.
I can see why the named types are needed for plugins, but a lot of what I see them used for is just for the
object
type registered as a schema and that one is a bit strange to me.
I was just experimenting with the new const type parameters , and that may also help tighten things down as we infer more and more!
I still have to push this with documentation but
user Q
, this just... works. No issues. Since the new infer type runs on the config directly we should theoretically be able to get types directly from
definePlugin
, eventually. I'm using documents instead of objects mainly because I don't know when `_type` is populated but, once I get that answer, this should work!
This looks.. amazing!Would this be drop-in to the or breaking change?
https://github.com/saiichihashimoto/sanity-typed/tree/main/packages/types
It's all pushed! I got it to include
definePlugin
types as well, so there's that! There's still some gaps but nothing new is broken, only things that weren't typed before continue to remain untyped. Regardless, it works, and it works well! There's a lot of tests, so you can browse through those and see how unchanged the schema API is.
I had to deviate a bit from sanity's own types to get these working, but not by much. I did my best to import
sanity
types directly so that any updates to sanity's types would make it in. I could start making PRs to sanity's codebase incrementally to get these in there, if I get the go ahead that that's welcome. I've tried to make the changes in a way that merging them in wouldn't break existing things, but I also don't want to invest the work if it's not something that sanity's interested in. Happy to run through it with whoever.
user J
great plugin :saluting_face: Can this plugin like creatw types to use on the front-end? Currently manually defining all the types. Thanks!
user Q
that’s what it’s for!
Will take a look and refactor docs and fields nice one!
Anyone following this thread, the front end client and groqs are now typed via @sanity-typed/client and @sanity-typed/groq.

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?