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

Explore Sanity TypeGen #610

Open
mathiazom opened this issue Sep 11, 2024 · 4 comments
Open

Explore Sanity TypeGen #610

mathiazom opened this issue Sep 11, 2024 · 4 comments
Assignees

Comments

@mathiazom
Copy link
Contributor

mathiazom commented Sep 11, 2024

We should consider adding Sanity TypeGen to generate TypeScript types based on the Sanity schema. This is instead of manually defining the types, e.g., based on inspection in Vision. We have already experienced that the types do not match the schema (especially when fields change names). With TypeGen, we can also use defineQuery for GROQ queries and get type inference on the return type (here we have also seen that it's easy to make mistakes).

This issue is only aimed at researching Sanity TypeGen and figure out if it's a good fit for the project. Depending on the conclusion, a second issue can be defined to actually implement it in the codebase.

@mathiazom mathiazom changed the title Sanity TypeGen Explore Sanity TypeGen Sep 11, 2024
@mathiazom mathiazom self-assigned this Sep 11, 2024
@mathiazom
Copy link
Contributor Author

mathiazom commented Sep 12, 2024

https://www.sanity.io/docs/sanity-typegen#4ed7457979d7

Unsupported schema that will be typed as unknown:

  • Cross-dataset references

Could be relevant for communication between studio and shared studio

@mathiazom
Copy link
Contributor Author

mathiazom commented Sep 12, 2024

There does not seem to be any way to configure a custom formatting of e.g. type names. This would be useful in cases were e.g. Compensations is both a React component and a generated type. There was a proposed change here to allow custom formatting, but this was rejected for now.

This can of course be worked around by renaming conflicting imports with as, e.g. import {Compensations as CompensationsType} from "sanity.types".

A more general approach would be to import all types under a common types import with import * as types from 'sanity.types'. But types.Compensations might be undesirable.

The naming of other parts of the codebase could also be reconsidered to reduce naming conflicts (e.g. CompensationsPage/CompensationsComponent/CompensationsView for the React component).

A final approach could be to augment the typegen process with a custom script. Either by altering the intermediate schema.json or the final sanity.types.ts.

@mathiazom
Copy link
Contributor Author

mathiazom commented Sep 17, 2024

Generated types can become highly nested, with only the top document type being named.

Excerpt from the Compensations document type:

export type Compensations = {
  _id: string;
  _type: "compensations";
  // ...
  bonusesByLocation?: Array<{
    location: {
      _ref: string;
      _type: "reference";
      _weak?: boolean;
      [internalGroqTypeReferenceTo]?: "companyLocation";
    };
    yearlyBonuses?: Array<{
      year: number;
      bonus: number;
      _type: "yearBonus";
      _key: string;
    }>;
    _type: "bonusData";
    _key: string;
  }>;
  // ...
}

The result is that subtypes can be cumbersome to access. For example, the following is required to access the generated type for an element within yearlyBonuses:

type YearlyBonus = NonNullable<NonNullable<Compensations['bonusesByLocation']>[number]['yearlyBonuses']>[number];

side note: can't use dot notation to access types, here is a nice explainer, and this explains the ...[number] syntax.

"worst-case" example:

export type NavigationFooterRichTextLinkChildMark = NonNullable<
  NonNullable<
    Extract<
      NonNullable<
        Extract<
          NonNullable<
            NonNullable<
              NonNullable<types.NAV_QUERYResult>["footer"]
            >[number]["linksAndContent"]
          >[number],
          { _type: "richTextObject" }
        >["richText"]
      >[number],
      { _type: "block" }
    >["children"]
  >[number]["marks"]
>[number];

this is a little contrived, in practice we would probably define this type in multiple steps to have some readability

This type aliasing introduces some manual labour (with somewhat exotic syntax), but hopefully still less than without type generation.

To organize these alias types along with the generated ones, we could define a types.ts like so:

import * as types from "./sanity.types";

export { types };

export type YearBonuses = NonNullable<
  NonNullable<types.Compensations["bonusesByLocation"]>[number]["yearlyBonuses"]
>;

giving the following import statement and usage:

import { types, YearBonuses } from "studio/types";

interface SomeProps {
  compensations: types.Compensations;
  bonuses: YearBonuses;
}

The same types.ts could be used for defining custom types in case of faulty type generation.

@mathiazom
Copy link
Contributor Author

Trying to consider potential lock-in risks:

One scenario could be that all types in the codebase are generated and used all over. And then suddently the typegen command fails with the codebase or produces some corrupt results. We would then have to fall back to defining custom types for any additions or changes to the schema. The obvious solution we be to copy the existing generated types and continue from there. The sudden removal of typegen could be problematic, but probably not a crisis.

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

No branches or pull requests

1 participant