sanity icon indicating copy to clipboard operation
sanity copied to clipboard

TypeScript types for schema

Open mickeyreiss opened this issue 4 years ago • 28 comments

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.

mickeyreiss avatar Apr 28 '20 00:04 mickeyreiss

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).

barbogast avatar May 18 '20 15:05 barbogast

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

neo avatar May 22 '20 21:05 neo

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.

mickeyreiss avatar May 23 '20 00:05 mickeyreiss

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).

barbogast avatar May 23 '20 12:05 barbogast

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.

AHBruns avatar Oct 04 '20 02:10 AHBruns

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

callumbooth avatar Apr 16 '21 09:04 callumbooth

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;

koriner avatar May 08 '21 01:05 koriner

+1

raptoria avatar May 17 '21 19:05 raptoria

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?

alvis avatar May 26 '21 02:05 alvis

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',
    },
  },
};

hypeJunction avatar Aug 25 '21 10:08 hypeJunction

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

alvis avatar Aug 25 '21 11:08 alvis

@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.

hypeJunction avatar Aug 25 '21 11:08 hypeJunction

There is also ricokahler/sanity-codegen.

sjelfull avatar Aug 26 '21 11:08 sjelfull

BUMP

whats the to go to in this space?

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

wommy avatar Feb 26 '22 01:02 wommy

I would LOVE this

saiichihashimoto avatar Feb 26 '22 02:02 saiichihashimoto

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

wommy avatar Feb 26 '22 03:02 wommy

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;
}

saiichihashimoto avatar Mar 27 '22 00:03 saiichihashimoto

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

julrich avatar Mar 27 '22 12:03 julrich

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

saiichihashimoto avatar Mar 27 '22 16:03 saiichihashimoto

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

phil-lgr avatar Apr 06 '22 14:04 phil-lgr

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 🤯🤯

wommy avatar Apr 08 '22 00:04 wommy

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

saiichihashimoto avatar May 16 '22 22:05 saiichihashimoto

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!

bline avatar Jun 08 '22 15:06 bline

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

kmelve avatar Jun 16 '22 16:06 kmelve

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.

saiichihashimoto avatar Jun 16 '22 17:06 saiichihashimoto

@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

han-tyumi avatar Jul 11 '22 22:07 han-tyumi

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

saiichihashimoto avatar Jul 12 '22 00:07 saiichihashimoto

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

saiichihashimoto avatar Jul 13 '22 17:07 saiichihashimoto

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

rexxars avatar Dec 12 '22 22:12 rexxars

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

magicspon avatar Dec 14 '22 20:12 magicspon