sanity-io / sanity

Sanity Studio – Rapidly configure content workspaces powered by structured content
https://www.sanity.io
MIT License
5.25k stars 427 forks source link

TypeScript types for schema #1857

Closed mickeyreiss closed 7 months ago

mickeyreiss commented 4 years ago

We are writing our site in typescript, using Gatsby and React. It would be great if the Sanity schema could be compiled to typescript interfaces, so that we could leverage static type safety during development.

I imagine generating this with a sanity cli command.

Given the translation layers between Sanity, GraphQL and TypeScript, I imagine this is is a subtle problem. If there's any more info I can provide to be helpful, let me know.

barbogast commented 4 years ago

We wrote something ourselves, until there are official type definitions: https://gist.github.com/barbogast/4bea3ad77272fafe0af3d4f70446d037

If you're interested we could create a repository and a NPM package. (Not sure how exactly to do this with TS, though).

neo commented 4 years ago

we could do a PR, or we could submit through https://github.com/DefinitelyTyped/DefinitelyTyped

mickeyreiss commented 4 years ago

Thanks for the responses. To clarify, the request here is not typescript types for writing out schemas, but rather typescript types for the types implied by the schema.

For example, I would want something like this for the Person example documented here:

namespace sanity {
    interface IPerson {
        // Name.
        name: string;
    }
}

This would allow type-checking for React components that use data from Sanity as props.

barbogast commented 4 years ago

Oh, okay. Do you want to use it for your client or within the studio? If it is for clients you probably want to have types for queries, not the schema. With both groq and graphql it is possible to reshape how the data actually arrives on the client. So the type definitions need to follow the query, not the schema (except if your queries always return the exact shape of the schema).

For graphql there might already be solutions out there (https://github.com/dotansimha/graphql-code-generator for example).

AHBruns commented 4 years ago

Oh, okay. Do you want to use it for your client or within the studio? If it is for clients you probably want to have types for queries, not the schema. With both groq and graphql it is possible to reshape how the data actually arrives on the client. So the type definitions need to follow the query, not the schema (except if your queries always return the exact shape of the schema).

For graphql there might already be solutions out there (https://github.com/dotansimha/graphql-code-generator for example).

Are there plans to add something similar for GROQ queries? It would be very nice to have end-to-end type safety without having to hand type everything.

callumbooth commented 3 years ago

For those using the GraphQL api and queries, its possible to use graphql code generator to create the types for you. Here is an example of the config file. This will grab the schema from the sanity api endpoint, look through your apps files for graphql queries and create a ts file with all the types for you.

#codegen.yaml

overwrite: true
schema: https://<projectId>.api.sanity.io/v1/graphql/<dataset>/<tag>
documents: "src/**/*.{ts,tsx,gql,graphql}"
generates:
  src/types/generated/graphcms-schema.ts:
    plugins:
      - typescript
      - typescript-operations
koriner commented 3 years ago

Is there official types for the Sanity document/schemas yet? I can't seem to figure out what type to use when defining a schema. I've tried something random like this but this not the right type.

import { SchemaType } from '@sanity/types';

const Page: SchemaType = {
  name: 'page',
  type: 'document',
  title: 'Page',
  fields: [
    {
      name: 'title',
      type: 'string',
      title: 'Title',
    },
    {
      name: 'description',
      type: 'string',
      title: 'Description',
    },
  ],
};

export default Page;
raptoria commented 3 years ago

+1

alvis commented 3 years ago

SchemaType is a close call but it's still not exactly what the schema is supposed to look like. @bjoerge @rexxars Would you be able to show us a direction?

hypeJunction commented 3 years ago

I am building on top of @barbogast schema. Should we create a collaborative repo out of it?

import * as React from 'react';
import { ElementType, ReactNode } from 'react';

type Meta = {
  parent: { [key: string]: any };
  path: string[];
  document: { [key: string]: any };
};

type CustomRuleCallback = (field: any, meta: Meta) => true | string | Promise<true | string>;

export type RuleType = {
  required: () => RuleType;
  custom: (cb: CustomRuleCallback) => RuleType;
  min: (min: number) => RuleType;
  max: (max: number) => RuleType;
  length: (exactLength: number) => RuleType;
  greaterThan: (gt: number) => RuleType;
  uri: (options: { scheme: string[] }) => RuleType;
  integer: () => RuleType;
  precision: (limit: number) => RuleType;
};

type Validation = (rule: RuleType) => RuleType | RuleType[];

export type CommonFieldProps = {
  title?: string;
  fieldset?: string;
  validation?: Validation;
  description?: string;
  hidden?: boolean;
  readOnly?: boolean;
  defaultValue?: any;
  inputComponent?: ElementType;
};

export type StringField<Name extends string = string> = CommonFieldProps & {
  name: Name;
  type: 'string';
  options?:
    | {
        list: { title: string; value: string }[] | string[];
        layout?: string;
      }
    | never;
};

export type NumberField<Name extends string = string> = CommonFieldProps & {
  name: Name;
  type: 'number';
  options?: {
    list: { title: string; value: string }[] | string[];
  };
};

export type TextField<Name extends string = string> = CommonFieldProps & {
  name: Name;
  type: 'text';
  rows?: number;
};

export type BooleanField<Name extends string = string> = CommonFieldProps & {
  name: Name;
  type: 'boolean';
  options?: {
    layout?: 'switch' | 'checkbox';
  };
};

export type DateField<Name extends string = string> = CommonFieldProps & {
  name: Name;
  type: 'date';
  options?: {
    dateFormat?: string;
  };
};

export type SlugField<Name extends string = string> = CommonFieldProps & {
  name: Name;
  type: 'slug';
  options?: {
    source?: string;
  };
};

export type UrlField<Name extends string = string> = CommonFieldProps & {
  name: Name;
  type: 'url';
};

export type BlockField<Name extends string = string> = CommonFieldProps & {
  name: Name;
  type: 'block';
  styles?: Array<{
    title: string;
    value: string;
    blockEditor?: {
      render: ElementType;
    };
    icon?: ElementType;
  }>;
  lists?: Array<{
    title: string;
    value: string;
  }>;
  marks?: {
    annotations?: ArrayOf[];
    decorators?: Array<{
      title: string;
      value: string;
      icon?: ElementType;
    }>;
  };
  of?: ArrayOf[];
  icon?: ElementType;
};

type ArrayOf = ObjectField | ReferenceField | ImageField | { type: string } | BlockField;

export type ArrayField<Name extends string = string> = CommonFieldProps & {
  name: Name;
  type: 'array';
  of: ArrayOf[];
};

type FilterFunctionResult = { filter: string; filterParams?: string };
type FilterFunction = (args: {
  document: { [key: string]: any };
  parentPath: string[];
  parent: Record<string, unknown>[];
}) => FilterFunctionResult;

type ReferenceField<Name extends string = string> = CommonFieldProps & {
  name: Name;
  type: 'reference';
  to: { type: string }[];
  options?: {
    filter: string | FilterFunction;
    filterParams?: { [key: string]: string };
  };
};

type ImageField<Name extends string = string> = CommonFieldProps & {
  name: Name;
  type: 'image';
  options?: {
    hotspot?: boolean;
  };
};

type FileField<Name extends string = string> = CommonFieldProps & {
  name: Name;
  type: 'file';
};

export type CustomField<Name extends string = string> = CommonFieldProps & {
  name: Name;
  type: 'money' | 'color' | 'icon' | 'iconPicker' | 'blockContent' | 'metadata';
  options?: Record<string, any>;
};

export type FieldCollection<T extends string> = Array<Field<T>>;

export type Field<Name extends string = string> =
  | StringField<Name>
  | NumberField<Name>
  | TextField<Name>
  | BooleanField<Name>
  | DateField<Name>
  | SlugField<Name>
  | UrlField<Name>
  | ArrayField<Name>
  | ReferenceField<Name>
  | ImageField<Name>
  | FileField<Name>
  | ObjectField<any, Name>
  | BlockField<Name>
  | CustomField<Name>;

type Preview = {
  select?: { [key: string]: string };
  prepare?: (selection: {
    [key: string]: any;
  }) => {
    title?: ReactNode;
    subtitle?: ReactNode;
    media?: ReactNode;
  };
  component?: React.VFC;
};

type Fieldset = {
  name: string;
  title: string;
  options?: { collapsible: boolean; collapsed?: boolean; columns?: number };
};

export type ObjectField<Schema extends any = any, Name extends string = string> = CommonFieldProps & {
  name: Name;
  type: 'object';
  title?: string;
  fields: FieldCollection<keyof Schema>;
  validation?: Validation;
  preview?: Preview;
  fieldsets?: Fieldset[];
  description?: string;
  options?: { collapsible?: boolean; collapsed?: boolean };
};

export type Document<T extends Record<string, any>> = {
  type: 'document';
  name: string;
  fields: FieldCollection<keyof T>;
  title?: string;
  validation?: Validation;
  preview?: Preview;
  fieldsets?: Fieldset[];
  initialValue?: { [key: string]: any };
  orderings?: {
    name: string;
    title: string;
    by: { field: string; direction: string }[];
  }[];
};

export type PreviewProps<T extends Record<string, any>> = {
  value: T;
};

export type Body2TextProps = { children: React.FunctionComponent<any> };
import { Substance } from '@fernarzt/cms-types';
import { Document, ObjectField } from '../../../types/createSchema';

export const SubstanceDocument: Document<Substance> = {
  type: 'document',
  name: 'Substance',
  title: 'Substances',
  fields: [
    {
      name: 'title',
      title: 'Title',
      type: 'string',
      validation: (rule) => rule.required(),
    },
    {
      name: 'slug',
      title: 'Slug',
      type: 'slug',
      options: {
        source: 'title',
      },
      validation: (rule) => rule.required(),
    },
    {
      name: 'codes',
      title: 'Codes',
      type: 'object',
      fields: [
        {
          name: 'act',
          title: 'ACT Code',
          type: 'object',
          fields: [
            {
              name: 'code',
              title: 'Code',
              type: 'string',
            },
            {
              name: 'title',
              title: 'Title',
              type: 'string',
            },
          ],
        },
      ],
    } as ObjectField<Substance['codes'], 'codes'>,
  ],
  preview: {
    select: {
      title: 'codes.act.code',
      subtitle: 'title',
    },
  },
};
alvis commented 3 years ago

@hypeJunction @bjoerge @rexxars shall we make it as a PR?

hypeJunction commented 3 years ago

@alvis We could fork, use a shared upstream, create a branch, and make a PR. We can work on the PR until it's universal enough. It's a bit hard to get all the type hints right.

sjelfull commented 3 years ago

There is also ricokahler/sanity-codegen.

wommy commented 2 years ago

BUMP

whats the to go to in this space?

@ricokahler 's plugin? was there any movement in the discussions above?

saiichihashimoto commented 2 years ago

I would LOVE this

wommy commented 2 years ago

https://sanity-codegen-dev.vercel.app/ saved my ass

import picoSanity from "picosanity"

type Query = (string | null)[]

export default picoSanity({
  projectId: process.env.SANITY_PROJECTID,
  dataset: process.env.SANITY_DATASET,
  apiVersion: '2022-02-22',
  useCdn: false,
}).fetch<Query>(`*[_type=='todo'] | order(_createdAt desc).title`)

edit: this looks hot 🔥🔥🔥 https://github.com/ricokahler/sanity-codegen/tree/alpha/packages/client

saiichihashimoto commented 2 years ago

I'm not sure how to put this in DefinitelyTyped (due to sanity parts having nonstandard import paths) but here's my types to share. It does the basics of all types, everything I haven't (or won't) get to is all typed unknown:

type MaybeArray<T> = T | T[];
type MaybePromise<T> = T | Promise<T>;

/** @link https://www.sanity.io/docs/validation */
interface Rule<ExtendingRule, Value> {
  custom: (
    validator: (value: Value, context: unknown) => MaybePromise<false | string>
  ) => ExtendingRule;
  error: (message: string) => ExtendingRule;
  required: () => ExtendingRule;
  valueOfField: (field: string) => unknown;
  warning: (message: string) => ExtendingRule;
}

interface LengthRule<ExtendingRule> {
  length: (exactLength: number) => ExtendingRule;
}

interface MinMaxRule<ExtendingRule> {
  max: (maxValue: number) => ExtendingRule;
  min: (minLength: number) => ExtendingRule;
}

interface StringContentRule<ExtendingRule> {
  lowercase: () => ExtendingRule;
  regex: (
    regex: RegExp,
    options?: {
      invert?: boolean;
      name?: string;
    }
  ) => ExtendingRule;
  uppercase: () => ExtendingRule;
}

interface ListItem<Value> {
  title: string;
  value: Value;
}

type ListItems<Value> = Value[] | ListItem<Value>[];

type ListOptions<Value> =
  | {
      layout?: "dropdown";
      list?: ListItems<Value>;
    }
  | {
      direction?: "horizontal" | "vertical";
      layout: "radio";
      list?: ListItems<Value>;
    };

interface NamedDef<Name extends string> {
  name: Name;
  title?: string;
}

/** @link https://www.sanity.io/docs/initial-value-templates */
interface WithInitialValue<Value> {
  initialValue?: Value | (() => Promise<Value>);
}

/** @link https://www.sanity.io/docs/conditional-fields */
type ConditionalField<Value> =
  | boolean
  | ((context: {
      currentUser: {
        email: string;
        id: string;
        name: string;
        profileImage: string;
        roles: {
          description?: string;
          name: string;
          title?: string;
        }[];
      };
      document?: unknown;
      parent?: unknown;
      value: Value;
    }) => boolean);

/** @link https://www.sanity.io/docs/schema-types */
interface FieldDef<Name extends string, Rule, Value>
  extends NamedDef<Name>,
    WithInitialValue<Value> {
  /** @link https://github.com/ricokahler/sanity-codegen/tree/alpha#schema-codegen-options */
  codegen?: { required: boolean };
  description?: string;
  hidden?: ConditionalField<Value>;
  readonly?: ConditionalField<Value>;
  /** @link https://www.sanity.io/docs/validation */
  validation?: (rule: Rule) => MaybeArray<Rule>;
}

/** @link https://www.sanity.io/docs/block-type#validation */
interface BlockRule extends Rule<BlockRule, unknown> {}

/** @link https://www.sanity.io/docs/block-type */
interface BlockFieldDef<Name extends string>
  extends FieldDef<Name, BlockRule, unknown> {
  icon?: (...args: unknown[]) => unknown;
  lists?: ListItem<unknown>[];
  marks?: unknown;
  of?: unknown[];
  options?: { spellCheck?: boolean };
  styles?: ListItem<unknown>[];
}

/** @link https://www.sanity.io/docs/boolean-type#validation */
interface BooleanRule extends Rule<BooleanRule, boolean> {}

/** @link https://www.sanity.io/docs/boolean-type */
interface BooleanFieldDef<Name extends string>
  extends FieldDef<Name, BooleanRule, boolean> {
  options?: { layout?: "checkbox" | "switch" };
  type: "boolean";
}

/** @link https://www.sanity.io/docs/date-type#validation */
interface DateRule extends Rule<DateRule, string> {}

/** @link https://www.sanity.io/docs/date-type */
interface DateFieldDef<Name extends string>
  extends FieldDef<Name, DateRule, string> {
  options?: {
    calendarTodayLabel?: string;
    dateFormat?: string;
  };
}

/** @link https://www.sanity.io/docs/datetime-type#validation */
interface DatetimeRule
  extends Rule<DatetimeRule, string>,
    MinMaxRule<DatetimeRule> {}

/** @link https://www.sanity.io/docs/datetime-type */
interface DatetimeFieldDef<Name extends string>
  extends FieldDef<Name, DatetimeRule, string> {
  options?: {
    calendarTodayLabel?: string;
    dateFormat?: string;
    timeFormat?: string;
    timeStep?: number;
  };
}

interface GeopointValue {
  alt: number;
  lat: number;
  lng: number;
}

/** @link https://www.sanity.io/docs/geopoint-type#validation */
interface GeopointRule extends Rule<GeopointRule, GeopointValue> {}

/** @link https://www.sanity.io/docs/geopoint-type */
interface GeopointFieldDef<Name extends string>
  extends FieldDef<Name, GeopointRule, GeopointValue> {
  options?: {
    calendarTodayLabel?: string;
    dateFormat?: string;
    timeFormat?: string;
    timeStep?: number;
  };
}

/** @link https://www.sanity.io/docs/number-type#validation */
interface NumberRule extends Rule<NumberRule, number>, MinMaxRule<NumberRule> {
  greaterThan: (limit: number) => NumberRule;
  integer: () => NumberRule;
  lessThan: (limit: number) => NumberRule;
  negative: () => NumberRule;
  positive: () => NumberRule;
  precision: (limit: number) => NumberRule;
}

/** @link https://www.sanity.io/docs/number-type */
interface NumberFieldDef<Name extends string>
  extends FieldDef<Name, NumberRule, number> {
  options?: ListOptions<number>;
  type: "number";
}

interface ReferenceValue {
  _ref: string;
  _type: "reference";
}

/** @link https://www.sanity.io/docs/reference-type#validation */
interface ReferenceRule extends Rule<ReferenceRule, ReferenceValue> {}

/** @link https://www.sanity.io/docs/reference-type */
interface ReferenceFieldDef<DocumentNames extends string, Name extends string>
  extends FieldDef<Name, ReferenceRule, ReferenceValue> {
  options?: {
    disableNew?: boolean;
  } & ({ filter?: string; filterParams?: object } & {
    filter?: (context: {
      document: unknown;
      parent: unknown;
      parentPath: string;
    }) => MaybePromise<{
      filter: string;
      params: unknown;
    }>;
  });
  to: { type: DocumentNames }[];
  type: "reference";
  weak?: boolean;
}

/** @link https://www.sanity.io/docs/slug-type#validation */
interface SlugRule extends Rule<SlugRule, string> {}

/** @link https://www.sanity.io/docs/slug-typen */
interface SlugDef<Name extends string>
  extends FieldDef<Name, SlugRule, string> {
  options?: {
    isUnique?: (value: string, options: unknown) => MaybePromise<boolean>;
    maxLength?: number;
    slugify?: (value: string, type: unknown) => MaybePromise<string>;
    source?:
      | string
      | ((context: {
          doc: unknown;
          options: {
            parent: unknown;
            parentPath: string;
          };
        }) => string);
  };
  type: "slug";
}

/** @link https://www.sanity.io/docs/string-type#validation */
interface StringRule
  extends Rule<StringRule, string>,
    LengthRule<StringRule>,
    MinMaxRule<StringRule>,
    StringContentRule<StringRule> {}

/** @link https://www.sanity.io/docs/string-type */
interface StringFieldDef<Name extends string>
  extends FieldDef<Name, StringRule, string> {
  options?: ListOptions<string>;
  type: "string";
}

/** @link https://www.sanity.io/docs/text-type#validation */
interface TextRule
  extends Rule<TextRule, string>,
    LengthRule<TextRule>,
    MinMaxRule<TextRule>,
    StringContentRule<TextRule> {}

/** @link https://www.sanity.io/docs/text-type */
interface TextFieldDef<Name extends string>
  extends FieldDef<Name, TextRule, string> {
  type: "text";
}

/** @link https://www.sanity.io/docs/url-type#validation */
interface URLRule extends Rule<URLRule, string> {
  uri: (options: {
    allowRelative?: boolean;
    relativeOnly?: boolean;
    scheme?: string[];
  }) => URLRule;
}

/** @link https://www.sanity.io/docs/url-type */
interface URLFieldDef<Name extends string>
  extends FieldDef<Name, URLRule, string> {
  type: "url";
}

type PrimitiveFieldDef<Name extends string> =
  | BooleanFieldDef<Name>
  | DateFieldDef<Name>
  | DatetimeFieldDef<Name>
  | NumberFieldDef<Name>
  | StringFieldDef<Name>
  | TextFieldDef<Name>
  | URLFieldDef<Name>;

type NonPrimitiveFieldDef<
  DocumentNames extends string,
  ObjectNames extends string,
  Name extends string,
  FieldNames extends string
> =
  /* eslint-disable no-use-before-define -- Circular dependency */
  | FileFieldDef<DocumentNames, ObjectNames, Name, FieldNames>
  | ImageFieldDef<DocumentNames, ObjectNames, Name, FieldNames>
  | ObjectFieldDef<DocumentNames, ObjectNames, Name, FieldNames, string, string>
  /* eslint-enable no-use-before-define */
  | GeopointFieldDef<Name>
  | ReferenceFieldDef<DocumentNames, Name>
  | SlugDef<Name>;

/** @link https://www.sanity.io/docs/array-type#validation */
interface ArrayRule
  extends Rule<ArrayRule, unknown[]>,
    LengthRule<ArrayRule>,
    MinMaxRule<ArrayRule> {
  unique: () => ArrayRule;
}

/** @link https://www.sanity.io/docs/array-type */
interface ArrayFieldDef<
  DocumentNames extends string,
  ObjectNames extends string,
  Name extends string
> extends FieldDef<Name, ArrayRule, unknown[]> {
  of:
    | Omit<BlockFieldDef<never>, "name">[]
    | Omit<PrimitiveFieldDef<never>, "name">[]
    | (
        | Omit<
            NonPrimitiveFieldDef<DocumentNames, ObjectNames, never, string>,
            "name"
          >
        | Omit<ReferenceFieldDef<DocumentNames, never>, "name">
        | {
            title?: string;
            type: ObjectNames;
          }
      )[];
  options?: {
    editModal?: "dialog" | "fullscreen";
    layout?: "grid" | "tags";
    list?: ListItem<string>[];
    sortable?: boolean;
  };
  type: "array";
}

type FieldType<
  DocumentNames extends string,
  ObjectNames extends string,
  Name extends string,
  FieldNames extends string
> =
  | ArrayFieldDef<DocumentNames, ObjectNames, Name>
  | NonPrimitiveFieldDef<DocumentNames, ObjectNames, Name, FieldNames>
  | PrimitiveFieldDef<Name>;

type FileValue<FieldNames extends string> = Record<
  Exclude<FieldNames, "_type" | "asset">,
  string
> & {
  _type: "file";
  asset: ReferenceValue;
};

/** @link https://www.sanity.io/docs/arfileray-type#validation */
interface FileRule<FieldNames extends string>
  extends Rule<FileRule<FieldNames>, FileValue<FieldNames>> {}

/** @link https://www.sanity.io/docs/file-type */
interface FileFieldDef<
  DocumentNames extends string,
  ObjectNames extends string,
  Name extends string,
  FieldNames extends string
> extends FieldDef<Name, FileRule<FieldNames>, FileValue<FieldNames>> {
  fields?: FieldType<DocumentNames, ObjectNames, FieldNames, string>[];
  options?: {
    /** @link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers */
    accept?: string;
    /** @link https://www.sanity.io/docs/custom-asset-sources */
    sources?: unknown[];
    storeOriginalFilename?: boolean;
  };
  type: "file";
}

type ImageValue<FieldNames extends string> = Record<
  Exclude<FieldNames, "_type" | "asset" | "crop" | "hotspot">,
  string
> & {
  _type: "image";
  asset: ReferenceValue;
  crop: {
    bottom: number;
    left: number;
    right: number;
    top: number;
  };
  hotspot: {
    height: number;
    width: number;
    x: number;
    y: number;
  };
};

/** @link https://www.sanity.io/docs/image-type#validation */
interface ImageRule<FieldNames extends string>
  extends Rule<ImageRule<FieldNames>, ImageValue<FieldNames>> {}

/** @link https://www.sanity.io/docs/image-type */
interface ImageFieldDef<
  DocumentNames extends string,
  ObjectNames extends string,
  Name extends string,
  FieldNames extends string
> extends FieldDef<Name, ImageRule<FieldNames>, unknown> {
  fields?: (FieldType<DocumentNames, ObjectNames, FieldNames, string> & {
    isHighlighted?: boolean;
  })[];
  options?: {
    /** @link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers */
    accept?: string;
    hotspot?: boolean;
    /** @link https://www.sanity.io/docs/image-metadata */
    metadata?: string[];
    /** @link https://www.sanity.io/docs/custom-asset-sources */
    sources?: unknown[];
    storeOriginalFilename?: boolean;
  };
  type: "image";
}

interface ObjectLikeDef<
  DocumentNames extends string,
  ObjectNames extends string,
  FieldNames extends string,
  FieldSetNames extends string,
  SelectionNames extends string,
  GroupNames extends string
> {
  fields: (FieldType<DocumentNames, ObjectNames, FieldNames, string> & {
    /** @link https://www.sanity.io/docs/object-type#AbjN0ykp */
    fieldset?: FieldSetNames;
    /** @link https://www.sanity.io/docs/field-groups */
    group?: MaybeArray<GroupNames>;
  })[];
  /** @link https://www.sanity.io/docs/object-type#AbjN0ykp */
  fieldsets?: {
    name: FieldSetNames;
    title: string;
  }[];
  /** @link https://www.sanity.io/docs/previews-list-views */
  preview?:
    | {
        select: {
          media?: string | unknown;
          subtitle?: FieldNames;
          title?: FieldNames;
        };
      }
    | {
        component?: (props: {
          [name in SelectionNames]: unknown;
        }) => unknown;
        prepare: (selection: {
          [name in SelectionNames]: unknown;
        }) => {
          media?: string | unknown;
          subtitle?: FieldNames;
          title?: FieldNames;
        };
        select: {
          [name in SelectionNames]: FieldNames;
        };
      };
}

/** @link https://www.sanity.io/docs/object-type#validation */
interface ObjectRule extends Rule<ObjectRule, unknown> {}

/** @link https://www.sanity.io/docs/object-type */
interface ObjectFieldDef<
  DocumentNames extends string,
  ObjectNames extends string,
  Name extends string,
  FieldNames extends string,
  FieldSetNames extends string,
  SelectionNames extends string
> extends FieldDef<Name, ObjectRule, unknown>,
    ObjectLikeDef<
      DocumentNames,
      ObjectNames,
      FieldNames,
      FieldSetNames,
      SelectionNames,
      never
    > {
  inputComponent?: unknown;
  type: "object";
}

type ObjectDef<
  // DocumentNames & ObjectNames reversed!!! Mostly for convenience when defining types
  ObjectNames extends string,
  DocumentNames extends string = never,
  FieldNames extends string = string,
  FieldSetNames extends string = string,
  SelectionNames extends string = string
> = ObjectFieldDef<
  DocumentNames,
  ObjectNames,
  ObjectNames,
  FieldNames,
  FieldSetNames,
  SelectionNames
>;

/** @link https://www.sanity.io/docs/document-type */
interface DocumentDef<
  DocumentNames extends string,
  ObjectNames extends string = never,
  FieldNames extends string = string,
  FieldSetNames extends string = string,
  SelectionNames extends string = string,
  GroupNames extends string = string
> extends NamedDef<DocumentNames>,
    WithInitialValue<unknown>,
    ObjectLikeDef<
      DocumentNames,
      ObjectNames,
      FieldNames,
      FieldSetNames,
      SelectionNames,
      GroupNames
    > {
  /** @link https://www.sanity.io/docs/field-groups */
  groups?: (NamedDef<string> & {
    default?: boolean;
    hidden?: ConditionalField<unknown>;
    icon?: unknown;
  })[];
  liveEdit?: boolean;
  /** @link https://www.sanity.io/docs/sort-orders */
  orderings?: (NamedDef<string> & {
    by: {
      direction: "asc" | "desc";
      field: FieldNames;
    }[];
  })[];
  type: "document";
}

type SchemaType<
  DocumentNames extends string = any,
  ObjectNames extends string = any,
  FieldNames extends string = string,
  FieldSetNames extends string = string,
  SelectionNames extends string = string,
  GroupNames extends string = string
> =
  | DocumentDef<
      DocumentNames,
      ObjectNames,
      FieldNames,
      FieldSetNames,
      SelectionNames,
      GroupNames
    >
  | ObjectDef<
      ObjectNames,
      DocumentNames,
      FieldNames,
      FieldSetNames,
      SelectionNames
    >;

declare module "@sanity/base" {
  export const ObjectDef;

  export const DocumentDef;

  export const SchemaType;
}

declare module "part:@sanity/base/schema-creator" {
  import type { Schema } from "@sanity/schema/dist/dts/legacy/Schema";

  const createSchema: <
    DocumentNames extends string,
    ObjectNames extends string
  >(schemaDef: {
    name: string;
    types: SchemaType<DocumentNames, ObjectNames>[];
  }) => Schema;

  export default createSchema;

  export const ObjectDef;

  export const DocumentDef;

  export const SchemaType;
}

declare module "all:part:@sanity/base/schema-type" {
  const schemaTypes: SchemaType[];

  export default schemaTypes;
}
julrich commented 2 years ago

Thanks a lot for sharing this! That could become quite handy! :pray:

saiichihashimoto commented 2 years ago

I’d like to include these in DefinitelyTyped but I’m not sure how. If anyone knows the answer to my issue, please help!

https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/59497

phil-lgr commented 2 years ago

Sorry to pollute this thread and the ironic tone, but ... IMO Sanity in 2022 should have 100% TypeScript support, hell, all the init/sample projects should be scaffolded in TS

What is the point of GROQ Graphql, etc if everything in sanity (by default) is in plain JS?

image

Does anyone want to write any schema or plugin without TS today? Coming from other CMEs and framework, I am baffled by those schema files with 0 autocomplete or types

Edit:

The official docs have so little on that topic: https://www.sanity.io/docs/using-typescript-in-sanity-studio

wommy commented 2 years ago

Sorry to pollute this thread and the ironic tone, but ... IMO Sanity in 2022 should have 100% TypeScript support, hell, all the init/sample projects should be scaffolded in TS

What is the point of GROQ Graphql, etc if everything in sanity (by default) is in plain JS?

image

Does anyone want to write any schema or plugin without TS today? Coming from other CMEs and framework, I am baffled by those schema files with 0 autocomplete or types

Edit:

The official docs have so little on that topic: https://www.sanity.io/docs/using-typescript-in-sanity-studio

100% this

my mind was so boggled by how they took all the time for TS yet didnt pass any of that on

still 🤯🤯

saiichihashimoto commented 2 years ago

I'm attempting to get some types pushed into Definitely Typed so there's a starting point for people to iterate on, feel free to push on it https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60376

bline commented 2 years ago

Has there been any update on this? Having end-to-end typings is pretty important IMHO. With sanity-codegen we can type the frontend based on the schema but without proper schema types, the bases for those typings is non-typed hand-written JS, certainly not an ideal situation.

Also, does anyone have any schema examples using the current loose types? Either builtin or contributed. Thanks!

kmelve commented 2 years ago

Just a short update to this thread! We just announced the Developer Preview of Sanity Studio v3 with new APIs that are fully typed, including the Schema API.

This might not be exactly what you want, so I'd also check out @saiichihashimoto's sanity-typed-schema-builder that he just posted in Awesome Sanity Studio v3

saiichihashimoto commented 2 years ago

sanity-typed-schema-builder is meant to type the frontend document types off of the schemas. Codegen was really bothering me so I wrote this library.

han-tyumi commented 2 years ago

@saiichihashimoto

I'd really like to use sanity-typed-schema-builder until v3 is released, but I'm having issues with it ~and it seems like the GitHub repo has been removed despite it being updated on NPM recently. So I'm not sure where else to ask for help with it~.

edit: see https://github.com/saiichihashimoto/sanity-typed-schema-builder/issues/113

saiichihashimoto commented 2 years ago

Hey @han-tyumi! I just made the repo public, can you add an issue into there? It being private was an accident.

saiichihashimoto commented 2 years ago

Not a full solution, but @han-tyumi found the issue in https://github.com/saiichihashimoto/sanity-typed-schema-builder/issues/113

rexxars commented 1 year ago

With Sanity studio v3 we're now shipping defineType() methods to help with this.

magicspon commented 1 year ago

What's the latest on this? Is there first class support for e2e type safety?

JannikWempe commented 1 year ago

With Sanity studio v3 we're now shipping defineType() methods to help with this.

@rexxars defineType() is helping a lot with creating the schema in a type-safe way but that is not what this issue is about. It is about TS types for consuming the data (as defined by the schema).

For GQL you could use graphql-codegen but there seems to be no easy solution to consume the data using GROQ in a type-safe way.

my-pal-jason commented 1 year ago

I'd like to suggest that this issue be re-opened @rexxars.

An official release of the sanity schema types returned by groq would be helpful. For instance, a slug has a _type and current field when returned from a groq query. I'd like to leverage a type for that.

In the cases where queries transform data, we can always alter the supplied types (Omit, etc), but the absence of any types for standard schemas is forcing a lot of boilerplate and confusion.

julrich commented 1 year ago

For another data point, don't see how defineType actually addresses what was expressed in this thread. Why was this closed?

There were some efforts from the community (@saiichihashimoto) to provide types for those gaps (e.g. here: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60376#issuecomment-1133767384). The answer by Sanity there could be read as "damage control", but apparently it resulted in that not being explored further, either?

If there are reasons for this being super hard / impossible, it would probably help being a bit more transparent about those here!

@saiichihashimoto: Were you able to work through your questions with them? Haven't tried https://github.com/saiichihashimoto/sanity-typed-schema-builder yet, would you say this could be used as a solid solution to fill this gap? We're currently still on a copy & pasted, semi-hand rolled type collection... would really like to switch this out for something more reliable, ideally something official.

saiichihashimoto commented 1 year ago

Every discussion around typescript and sanity I've seen gets a little lost in what's being discussed, so here's a few things that can get typed:

Ultimately, defineType solves typing the schemas and nothing else. It's also new with v3, so it hasn't really been factored into the other decisions. sanity-codegen and sanity-typed-schema-builder (I prefer this one, but I made it) solves typing the documents. I think sanity-codegen also solves typing the groqs.

Typed schemas has been solved in the best way possible: the code owners are doing it. Maintenance and contributions all go to the best place. I believe the documents can be typed better. I made the builder as a (in my opinion) better way of typing documents than codegen. However, I do believe there's probably an even better way, where we can derive the types from the schemas, now that they're more strongly typed from defineType.

I believe something similar about typing groq results. codegen works, although I'm a strong believer that this can be solved in application code, although not completely through deriving types (deriving result types from a groq string directly sounds truly impossible). I think the builder method is most appropriate, where a library is given the possible document types and can use builder methods to generate both the groq string and the type of the result.

That being said, none of my beliefs have been substantiated or worked on. I've thought of doing that but haven't put in any of the time. Ultimately, typing sanity has been solved for the three problems I've mentioned, albeit not by sanity itself. Whether that means this issue has truly been completed or not is beyond me: that's more a matter of scope and priority. I'd prefer sanity to find means of solving this directly, especially since investing in typescript is clearly not something they're avoiding with defineType. It's likely on the roadmap! Any library that comes along for deriving document types likely has a strong chance of just being merged into sanity. A groq builder likely won't get merged in (unless they agree a builder is appropriate and something they want to maintain) but is ultimately a good idea.

/rant

saiichihashimoto commented 1 year ago

In terms of your question to me, I've loosely asked about sanity typing their own documents, but you're seeing what I've seen. I'm sure they want it but I can't imagine it's priority number 1. If you look through their types they use for schemas, you can see that each field somewhat has to type the value in there as well, which is why I believe there must be a way to derive these types. My only question is whether they're prioritizing it or not, so we can know if it's worth it to make an open source package that does this or if it's wasted effort.

julrich commented 1 year ago

Thanks for that detailed answer! Indeed think it's mainly about priorities. I have no doubt in their general chops to be able to do this! The whole way this was communicated just rubbed me the wrong way, somehow. Mainly how the other issue just ended with your (imho) very reasonable comment... just hanging there. And this one being closed pretty unceremoniously, without mentioning if / what plans there are for other use cases expressed in this very issue :/

kmelve commented 1 year ago

Excellent summary @saiichihashimoto!

Know that we are actively considering our TypeScript story related to different modes of the product (from typing of libraries to consuming typed data from APIs).

For people interested in e2e type(script)-safety of content/data from Content Lake, I suggest hopping into this thread https://github.com/sanity-io/client/issues/89 where we have a proposal with runnable examples.

saiichihashimoto commented 1 year ago

I didn't know where to post this, but I thought I'd share:

However, I do believe there's probably an even better way, where we can derive the types from the schemas, now that they're more strongly typed from defineType.

I've looked into this, and it doesn't look possible as of now. It amounts to being able to derive the value from each defineField, defineType, and defineArrayMember which isn't complicated when it's one of the native values (relatively simple). It becomes complicated for any type that composes another type. For example, it's easy to get a defineField({ name: 'foo', type: 'url' }) and derive { foo?: string }. This is because UrlDefinition has to type it's value string in there somewhere, which can be pulled out.

The issue becomes when we need to compose types together, which we will always do. For example, an array type is currently impossible to type (without changing the current API). ArrayDefinition has no generics, so it doesn't carry any information about what ends up in the of array, so it will always have a value of either unknown[] or any[], which isn't helpful. This is also true for all the types containing fields: ObjectDefinition, DocumentDefinition, FileDefinition, and ImageDefinition. In sanity-typed-schema-builder, I had to have generics at those levels, effectively "bubbling" their field's/member's types upward so they're available.

Also, there's no good way to identify whether a field is optional or required. It's all under the validation field, and it returns the same type regardless. Without it's typing changing, we can't identify this either. We could do the codegen method of adding a required: true field onto these schemas... but no.

I've thought of using module augmentation to accomplish this, but we'd need to narrow the types, which you can't do through augmentation. I don't think there's a way around this.

bjoerge commented 1 year ago

Reopening this since the original ask is still valid. As pointed out by several others in this thread, defineType() doesn't help with inferring document/value types from Sanity schemas.

saiichihashimoto commented 1 year ago

Not to advertise my own projects, but I've been attempting to solve this in sanity-typed and I think it's been a pretty solid solution, although it has it's gaps.

I know the sanity team has been thinking about typing (especially groqs) but, as far as I know, my project and groqd are the two big ones for typing anything sanity data related.

kmelve commented 7 months ago

Great news, everyone – and thanks for your patience!

We have released Sanity TypeGen.

You can learn more here: