@sanity-typed/types
Version:
Infer Sanity Document Types from Sanity Schemas
514 lines (412 loc) • 18.1 kB
Markdown
# -typed/types
[](https://www.npmjs.com/package/@sanity-typed/types)
[](https://github.com/saiichihashimoto/sanity-typed/pulls?q=is%3Apr+is%3Aclosed)
[](https://github.com/saiichihashimoto/sanity-typed/stargazers)
[](https://github.com/saiichihashimoto/sanity-typed/graphs/contributors)
[](https://github.com/saiichihashimoto/sanity-typed/labels/help%20wanted)
[](https://www.npmjs.com/package/@sanity-typed/types?activeTab=code)
[](LICENSE)
[](https://github.com/sponsors/saiichihashimoto)
Infer Sanity Document Types from Sanity Schemas
[](https://github.com/saiichihashimoto/sanity-typed/assets/2819256/13c28e6a-74a7-4b3c-8162-61fae921323b)
## Page Contents
- [Install](#install)
- [Usage](#usage)
- [`DocumentValues`](#documentvalues)
- [Plugins](#plugins)
- [Writing typed plugins](#writing-typed-plugins)
- [Using external untyped plugins](#using-external-untyped-plugins)
- [Considerations](#considerations)
- [Types match config but not actual documents](#types-match-config-but-not-actual-documents)
- [Typescript Errors in IDEs](#typescript-errors-in-ides)
- [VSCode](#vscode)
- [Breaking Changes](#breaking-changes)
- [7 to 8](#7-to-8)
- [Typescript version from 5.7.2 <= x <= 5.7.3](#typescript-version-from-572--x--573)
- [6 to 7](#6-to-7)
- [Typescript version from 5.4.2 <= x <= 5.6.3](#typescript-version-from-542--x--563)
- [`as const` needed for certain types to infer correctly](#as-const-needed-for-certain-types-to-infer-correctly)
- [6 no longer forces `as const`](#6-no-longer-forces-as-const)
- [5 to 6](#5-to-6)
- [Block fields require `as const`](#block-fields-require-as-const)
- [4 to 5](#4-to-5)
- [Removed `_InferValue` and `AliasValue`](#removed-_infervalue-and-aliasvalue)
- [3 to 4](#3-to-4)
- [Referenced `_type` needs `as const`](#referenced-_type-needs-as-const)
- [Renamed `DocumentValue` to `SanityDocument`](#renamed-documentvalue-to-sanitydocument)
- [2 to 3](#2-to-3)
- [InferSchemaValues](#inferschemavalues)
- [InferValue](#infervalue)
- [Alternatives](#alternatives)
## Install
```bash
npm install sanity -typed/types
```
## Usage
Use `defineConfig`, `defineType`, `defineField`, and `defineArrayMember` from this library exactly as you would from [`sanity`](https://www.sanity.io/docs/schema-field-types#e5642a3e8506). Then, use `InferSchemaValues` to get the typescript types!
```product.ts```:
```typescript
// import { defineArrayMember, defineField, defineType } from "sanity";
import {
defineArrayMember,
defineField,
defineType,
} from "@sanity-typed/types";
/** No changes using defineType, defineField, and defineArrayMember */
export const product = defineType({
name: "product",
type: "document",
title: "Product",
fields: [
defineField({
name: "productName",
type: "string",
title: "Product name",
validation: (Rule) => Rule.required(),
}),
defineField({
name: "tags",
type: "array",
title: "Tags for item",
of: [
defineArrayMember({
type: "object",
name: "tag",
fields: [
defineField({ type: "string", name: "label" }),
defineField({ type: "string", name: "value" }),
],
}),
],
}),
],
});
```
```sanity.config.ts```:
```typescript
import { structureTool } from "sanity/structure";
// import { defineConfig } from "sanity";
import { defineConfig } from "@sanity-typed/types";
import type { InferSchemaValues } from "@sanity-typed/types";
import { post } from "./schemas/post";
import { product } from "./schemas/product";
/** No changes using defineConfig */
const config = defineConfig({
projectId: "59t1ed5o",
dataset: "production",
plugins: [structureTool()],
schema: {
types: [
product,
// ...
post,
],
},
});
export default config;
/** Typescript type of all types! */
export type SanityValues = InferSchemaValues<typeof config>;
/**
* SanityValues === {
* product: {
* _createdAt: string;
* _id: string;
* _rev: string;
* _type: "product";
* _updatedAt: string;
* productName: string;
* tags?: {
* _key: string;
* _type: "tag";
* label?: string;
* value?: string;
* }[];
* };
* // ... all your types!
* }
*/
```
## `DocumentValues`
While `InferSchemaValues` gives you all the types for a given config keyed by type, sometimes you just want a union of all the `SanityDocument`s. Drop it into `DocumentValues`:
```typescript
import type { DocumentValues, InferSchemaValues } from "@sanity-typed/types";
const config = defineConfig({
/* ... */
});
type SanityValues = InferSchemaValues<typeof config>;
/**
* SanityValues === { [type: string]: TypeValueButSomeTypesArentDocuments }
*/
type SanityDocuments = DocumentValues<SanityValues>;
/**
* SanityDocuments === Each | Document | In | A | Union
*/
```
## Plugins
### Writing typed plugins
Use `definePlugin` from this library exactly as you would from [sanity's own exports](https://www.sanity.io/docs/developing-plugins).
`my-plugin.ts`:
```typescript
// import { defineField, definePlugin, defineType } from "sanity";
import { defineField, definePlugin, defineType } from "@sanity-typed/types";
/** No changes using definePlugin */
export const myPlugin = definePlugin({
name: "plugin",
schema: {
types: [
defineType({
name: "myPlugin",
type: "object",
fields: [
defineField({
name: "baz",
type: "boolean",
}),
],
}),
],
},
});
```
`sanity.config.ts`:
```typescript
// import { defineConfig, defineField, defineType } from "sanity";
import { defineConfig, defineField, defineType } from "@sanity-typed/types";
import type { InferSchemaValues } from "@sanity-typed/types";
import { myPlugin } from "./my-plugin";
const foo = defineType({
name: "foo",
type: "document",
fields: [
defineField({
name: "bar",
type: "myPlugin",
}),
],
});
const config = defineConfig({
schema: {
types: [foo],
},
plugins: [myPlugin()],
});
export default config;
type SanityValues = InferSchemaValues<typeof config>;
export type Foo = SanityValues["foo"];
/**
* Foo === {
* _createdAt: string;
* _id: string;
* _rev: string;
* _type: "foo";
* _updatedAt: string;
* bar?: {
* _type: "myPlugin";
* baz?: boolean;
* };
* };
**/
```
However, this export won't work for users who are using sanity's default methods. So that you won't have to define your plugin twice, we provide a `castFromTyped` method, which converts the outputs of any `define*` method to their native `sanity` counterparts:
```typescript
import { castFromTyped, definePlugin } from "@sanity-typed/types";
export const myTypedPlugin = definePlugin({
name: "plugin",
schema: {
types: [
// ...
],
},
});
// You'll likely want this as a default export as well!
export const myUntypedPlugin = castFromTyped(myTypedPlugin);
```
### Using external untyped plugins
sanity-typed also works directly with untyped `definePlugin` directly, so you can import and use plugins directly (although they type as `unknown` values). It doesn't handle untyped `defineField`/`defineArrayMember`/`defineType` though, and some plugins export some for convenience. `castToTyped` similarly converts untyped `define*` methods to `sanity-typed` versions with `unknown` values:
```typescript
import { orderRankField } from "@sanity/orderable-document-list";
import { castToTyped } from "@sanity-typed/types";
const nav = defineType({
name: "nav",
type: "document",
title: "Navigation",
fields: [
castToTyped(orderRankField({ type: "nav" })),
defineField({
name: "name",
type: "string",
title: "Name",
validation: (Rule) => Rule.required(),
}),
],
});
```
## Considerations
### Types match config but not actual documents
As your sanity driven application grows over time, your config is likely to change. Keep in mind that you can only derive types of your current config, while documents in your Sanity Content Lake will have shapes from older configs. This can be a problem when adding new fields or changing the type of old fields, as the types won't can clash with the old documents.
Ultimately, there's nothing that can automatically solve this; we can't derive types from a no longer existing config. This is a consideration with or without types: your application needs to handle all existing documents. Be sure to make changes in a backwards compatible manner (ie, make new fields optional, don't change the type of old fields, etc).
Another solution would be to keep old configs around, just to derive their types:
```typescript
const config = defineConfig({
schema: {
types: [foo],
},
plugins: [myPlugin()],
});
const oldConfig = defineConfig({
schema: {
types: [oldFoo],
},
plugins: [myPlugin()],
});
type SanityValues =
| InferSchemaValues<typeof config>
| InferSchemaValues<typeof oldConfig>;
```
This can get unwieldy although, if you're diligent about data migrations of your old documents to your new types, you may be able to deprecate old configs and remove them from your codebase.
### Typescript Errors in IDEs
Often you'll run into an issue where you get typescript errors in your IDE but, when building workspace (either you studio or app using types), there are no errors. This only occurs because your IDE is using a different version of typescript than the one in your workspace. A few debugging steps:
#### VSCode
- The [`JavaScript and TypeScript Nightly` extension (identifier `ms-vscode.vscode-typescript-next`)](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-typescript-next) creates issues here by design. It will always attempt to use the newest version of typescript instead of your workspace's version. I ended up uninstalling it.
- [Check that VSCode is actually using your workspace's version](https://code.visualstudio.com/docs/typescript/typescript-compiling#_compiler-versus-language-service) even if you've [defined the workspace version in `.vscode/settings.json`](https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-the-workspace-version-of-typescript). Use `TypeScript: Select TypeScript Version` to explictly pick the workspace version.
- Open any typescript file and you can [see which version is being used in the status bar](https://code.visualstudio.com/docs/typescript/typescript-compiling#_compiler-versus-language-service). Please check this (and provide a screenshot confirming this) before creating an issue. Spending hours debugging your issue ony to find that you're not using your workspace's version is very frustrating.
## Breaking Changes
### 7 to 8
#### Typescript version from 5.7.2 <= x <= 5.7.3
The supported Typescript version is now 5.7.2 <= x <= 5.7.3. Older versions are no longer supported and newer versions will be added as we validate them.
### 6 to 7
#### Typescript version from 5.4.2 <= x <= 5.6.3
The supported Typescript version is now 5.4.2 <= x <= 5.6.3. Older versions are no longer supported and newer versions will be added as we validate them.
#### `as const` needed for certain types to infer correctly
Like mentioned in [6 no longer forces `as const`](#6-no-longer-forces-as-const), `as const` is no required anywhere excent for references (otherwise they wouldn't reference correctly), but you will still want them in many places. Literals where it narrows the type are the usual candidates (ie string and number lists). But there are a few others, ie `options.hotspot` for the image type needs it to be typed as `true` to infer the hotspot fields. Due to typescript quirks, sometimes you'll need to add `true as const` for it to infer correctly.
Until we get a proper understanding on how we can force typescript to infer the literals, we won't enforce it anywhere except for references. This is because it's a convenience everywhere else; references are rarely what you want without it.
### 6 no longer forces `as const`
Besides for references, `as const` is no longer needed for some of the types. While it will still type string literals when possible, it won't be required. You'll still need `as const` if you actually want the literal types, but it was breaking too many valid workflows to require it.
### 5 to 6
#### Block fields require `as const`
Similar to references, to get the right types out of a block, we'll need `as const` with `styles[number].value` and `lists[number].value`. Also, `marks.annotations[number]` now requires typing like other array members, ie `defineArrayMember`:
```diff
const foo = defineType({
name: "foo",
type: "array",
of: [
defineArrayMember({
type: "block",
styles: [
- { title: "Foo", value: "foo" },
+ { title: "Foo", value: "foo" as const },
- { title: "Bar", value: "bar" },
+ { title: "Bar", value: "bar" as const },
],
lists: [
- { title: "Foo", value: "foo" },
+ { title: "Foo", value: "foo" as const },
- { title: "Bar", value: "bar" },
+ { title: "Bar", value: "bar" as const },
],
marks: {
annotations: [
- {
+ defineArrayMember({
name: "internalLink",
type: "object",
fields: [
- {
+ defineField({
name: "reference",
type: "reference",
- to: [{ type: "post" }],
+ to: [{ type: "post" as const }],
- },
+ }),
],
- },
+ }),
],
},
}),
],
});
```
### 4 to 5
#### Removed `_InferValue` and `AliasValue`
Use [`InferSchemaValues`](#inferschemavalues) instead. Neither `_InferValue` nor `AliasValue` are directly usable, while `InferSchemaValues` is the only real world use case.
### 3 to 4
#### Referenced `_type` needs `as const`
For `-typed/groq` to infer the right types from references, the reference type needs to carry the type it's referencing along with it. Unfortunately, it isn't deriving the literal so an `as const` is needed.
```diff
const product = defineType({
name: "product",
type: "document",
title: "Product",
fields: [
defineField({
name: "foo",
type: "reference",
- to: [{ type: "referencedType" }],
+ to: [{ type: "referencedType" as const }],
}),
],
});
```
#### Renamed `DocumentValue` to `SanityDocument`
```diff
- import type { DocumentValue } from "@sanity-typed/types";
+ import type { SanityDocument } from "@sanity-typed/types";
```
### 2 to 3
#### InferSchemaValues
`InferSchemaValues<typeof config>` used to return a union of all types but now returns an object keyed off by type. This is because using `Extract` to retrieve specific type was difficult. Object types would have a `_type` for easy extraction, but all the other types were less reliable (i.e. arrays and primitives).
```diff
export default config;
type Values = InferSchemaValues<typeof config>;
- export type Product = Extract<Values, { _type: "product" }>
+ export type Product = Values["product"];
```
#### InferValue
Types used to be inferred using `InferValue<typeof type>` for easy exporting. Now, `InferSchemaValues<typeof config>` needs to be used, and individual types keyed off of it. The reason for this is that only the config has context about aliased types, so `InferValue` was always going to be missing those values.
```diff
const product = defineType({
name: "product",
type: "document",
title: "Product",
fields: [
// ...
],
});
- export type Product = InferValue<typeof product>;
const config = defineConfig({
// ...
schema: {
types: [
product,
// ...
],
},
});
export default config;
type Values = InferSchemaValues<typeof config>;
+ export type Product = Values["product"];
```
You can still use `_InferValue` but this is discouraged, because it will be missing the context from the config (and is removed in v5):
```diff
const product = defineType({
name: "product",
type: "document",
title: "Product",
fields: [
// ...
],
});
- export type Product = InferValue<typeof product>;
+ export type Product = _InferValue<typeof product>;
```
## Alternatives
- [`sanity-codegen`](https://www.npmjs.com/package/sanity-codegen)
- [`-codegen/cli`](https://www.npmjs.com/package/@sanity-codegen/cli)
- [`sanity-generator`](https://www.npmjs.com/package/sanity-generator)
- [`sanity-typed-queries`](https://www.npmjs.com/package/sanity-generator)
- [`sanity-typed-schema`](https://www.npmjs.com/package/sanity-typed-schema)
- [`sanity-schema-builder`](https://www.npmjs.com/package/sanity-typed-schema)
- [`-typed/schema-builder`](https://www.npmjs.com/package/@sanity-typed/schema-builder)
- [`sanity-typed-schema-builder`](https://www.npmjs.com/package/sanity-typed-schema-builder)