Gathering detailed insights and metrics for @sanity-typed/client
Gathering detailed insights and metrics for @sanity-typed/client
Gathering detailed insights and metrics for @sanity-typed/client
Gathering detailed insights and metrics for @sanity-typed/client
Completing sanity's developer experience with typescript (and more)!
npm install @sanity-typed/client
Typescript
Module System
Node Version
NPM Version
@sanity-typed/next-sanity@4.0.2
Updated on May 27, 2025
@sanity-typed/groq@3.0.2
Updated on May 27, 2025
@sanity-typed/faker@4.0.2
Updated on May 27, 2025
@sanity-typed/client-mock@4.0.2
Updated on May 27, 2025
@sanity-typed/client@5.0.2
Updated on May 27, 2025
@sanity-typed/groq-js@3.0.2
Updated on May 27, 2025
TypeScript (99.94%)
JavaScript (0.06%)
Total Downloads
0
Last Day
0
Last Week
0
Last Month
0
Last Year
0
MIT License
158 Stars
1,706 Commits
8 Forks
3 Watchers
5 Branches
5 Contributors
Updated on Jul 10, 2025
Latest Version
5.0.2
Package Id
@sanity-typed/client@5.0.2
Unpacked Size
47.46 kB
Size
11.18 kB
File Count
7
NPM Version
10.8.2
Node Version
20.19.1
Published on
May 27, 2025
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
2
8
1
@sanity/client with typed GROQ Results
groqd
(actually groq-builder
)1npm install sanity @sanity-typed/client
Use createClient
exactly as you would from @sanity/client
.
product.ts
:
1// import { defineArrayMember, defineField, defineType } from "sanity"; 2import { 3 defineArrayMember, 4 defineField, 5 defineType, 6} from "@sanity-typed/types"; 7 8/** No changes using defineType, defineField, and defineArrayMember */ 9export const product = defineType({ 10 name: "product", 11 type: "document", 12 title: "Product", 13 fields: [ 14 defineField({ 15 name: "productName", 16 type: "string", 17 title: "Product name", 18 validation: (Rule) => Rule.required(), 19 }), 20 defineField({ 21 name: "tags", 22 type: "array", 23 title: "Tags for item", 24 of: [ 25 defineArrayMember({ 26 type: "object", 27 name: "tag", 28 fields: [ 29 defineField({ type: "string", name: "label" }), 30 defineField({ type: "string", name: "value" }), 31 ], 32 }), 33 ], 34 }), 35 ], 36});
sanity.config.ts
:
1import { structureTool } from "sanity/structure"; 2 3// import { defineConfig } from "sanity"; 4import { defineConfig } from "@sanity-typed/types"; 5import type { InferSchemaValues } from "@sanity-typed/types"; 6 7import { post } from "./schemas/post"; 8import { product } from "./schemas/product"; 9 10/** No changes using defineConfig */ 11const config = defineConfig({ 12 projectId: "59t1ed5o", 13 dataset: "production", 14 plugins: [structureTool()], 15 schema: { 16 types: [ 17 product, 18 // ... 19 post, 20 ], 21 }, 22}); 23 24export default config; 25 26/** Typescript type of all types! */ 27export type SanityValues = InferSchemaValues<typeof config>; 28/** 29 * SanityValues === { 30 * product: { 31 * _createdAt: string; 32 * _id: string; 33 * _rev: string; 34 * _type: "product"; 35 * _updatedAt: string; 36 * productName: string; 37 * tags?: { 38 * _key: string; 39 * _type: "tag"; 40 * label?: string; 41 * value?: string; 42 * }[]; 43 * }; 44 * // ... all your types! 45 * } 46 */
client.ts
:
1import type { SanityValues } from "sanity.config"; 2 3// import { createClient } from "@sanity/client"; 4import { createClient } from "@sanity-typed/client"; 5 6// export const client = createClient({ 7export const client = createClient<SanityValues>({ 8 projectId: "59t1ed5o", 9 dataset: "production", 10 useCdn: true, 11 apiVersion: "2023-05-23", 12}); 13 14export const makeTypedQuery = async () => 15 client.fetch('*[_type=="product"]{_id,productName,tags}'); 16/** 17 * typeof makeTypedQuery === () => Promise<{ 18 * _id: string; 19 * productName: string; 20 * tags: { 21 * _key: string; 22 * _type: "tag"; 23 * label?: string; 24 * value?: string; 25 * }[] | null; 26 * }[]> 27 */
groqd
(actually groq-builder
)@scottrippey is working on an amazing typed groqd
called groq-builder
, a schema-aware, strongly-typed GROQ query builder with auto-completion and type-checking for your GROQ queries. When given a function, fetch
will provide a GROQ builder for your use:
1npm install groq-builder
client-with-groq-builder.ts
:
1import type { SanityValues } from "sanity.config"; 2 3import { createClient } from "@sanity-typed/client"; 4 5export const client = createClient<SanityValues>({ 6 projectId: "59t1ed5o", 7 dataset: "production", 8 useCdn: true, 9 apiVersion: "2023-05-23", 10}); 11 12export const makeTypedQuery = async () => 13 /** No need for createGroqBuilder, `q` is already typed! */ 14 client.fetch((q) => 15 q.star 16 .filterByType("product") 17 .project({ _id: true, productName: true, tags: true }) 18 ); 19/** 20 * typeof makeTypedQuery === () => Promise<{ 21 * _id: string; 22 * productName: string; 23 * tags: { 24 * _key: string; 25 * _type: "tag"; 26 * label?: string; 27 * value?: string; 28 * }[] | null; 29 * }[]> 30 */
It will use the returned query
and parse
directly so you get typed results and runtime validation.
Deciding between using groq-builder
or directly typed queries is your decision! There are pros or cons to consider:
@sanity-typed/groq
does, which can run into strange errors. Meanwhile, a builder is typescript-first, allowing for complex structures without any issues.@sanity-typed/groq
had to be written, it can't do any auto-completion in IDEs like groq-builder
can. There was no way around this. Typed objects and methods are going to be superior to parsing a string. Again, typescript wasn't made for it.groq-builder
handles all GROQ operations.groq-builder
is currently in beta. You'll need to reference groqd
's documentation and sometimes they don't match 1-to-1.Sometimes, you'll have a preconfigured client from a separate library that you will still want typed results from. A castToTyped
function is provided to do just that.
1import { createClient } from "some-other-create-client"; 2 3import { castToTyped } from "@sanity-typed/client"; 4 5import type { SanityValues } from "./sanity.config"; 6 7const client = createClient({ 8 // ... 9}); 10 11const typedClient = castToTyped<SanityValues>()(client); 12 13// Also, if you need the config in the client (eg. for queries using $param), 14// you can provide the same config again to include it in the types. 15 16// const typedClient = castToTyped<SanityValues>()(client, { 17// ...same contents from createClient 18// }); 19 20const data = await typedClient.fetch("*"); 21/** 22 * typeof data === { 23 * _createdAt: string; 24 * _id: string; 25 * _rev: string; 26 * _type: "product"; 27 * _updatedAt: string; 28 * productName?: string; 29 * tags?: { 30 * _key: string; 31 * label?: string; 32 * value?: string; 33 * }[]; 34 * }[] 35 */
This function (nor the createClient
function) have any runtime implications; it passes through the initial client unaltered.
Similarly, if you have a typed client that you want to untype (presumably to export from a library for general consumption), you can always cast it:
1import type { SanityClient as SanityClientNative } from "@sanity/client"; 2 3import { createClient } from "@sanity-typed/client"; 4 5import type { SanityValues } from "./sanity.config"; 6 7const client = createClient<SanityValues>({ 8 // ... 9}); 10 11export const typedClient = client; 12 13export const untypedClient = client as SanityClientNative; 14 15export default untypedClient;
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:
1const config = defineConfig({ 2 schema: { 3 types: [foo], 4 }, 5 plugins: [myPlugin()], 6}); 7 8const oldConfig = defineConfig({ 9 schema: { 10 types: [oldFoo], 11 }, 12 plugins: [myPlugin()], 13}); 14 15type SanityValues = 16 | InferSchemaValues<typeof config> 17 | 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.
Similar to parsing, evaluating groq queries will attempt to match how sanity actually evaluates queries. Again, any fixes to match that or changes to groq evaluation will likely not be considered a major change but, rather, a bug fix.
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:
JavaScript and TypeScript Nightly
extension (identifier 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..vscode/settings.json
. Use TypeScript: Select TypeScript Version
to explictly pick the workspace version.Type instantiation is excessively deep and possibly infinite
🚨 CHECK Typescript Errors in IDEs
FIRST!!! ISSUES WILL GET CLOSED IMMEDIATELY!!! 🚨
You might run into the dreaded Type instantiation is excessively deep and possibly infinite
error when writing GROQ queries. This isn't too uncommon with more complex GROQ queries. Unfortunately, this isn't a completely avoidable problem, as typescript has limits on complexity and parsing types from a string is an inherently complex problem. A set of steps for a workaround:
@ts-expect-error
to disable the error. You could use @ts-ignore
instead, but ideally you'd like to remove the comment if a fix is released.groq/src/specific-issues.test.ts
with your issue. #642 is a great example for this. Try to reduce your query and config as much as possible. The goal is a minimal reproduction.People will sometimes create a repo with their issue. Please open a PR with a minimal test instead. Without a PR there will be no tests reflecting your issue and it may appear again in a regression. Forking a github repo to make a PR is a more welcome way to contribute to an open source library.
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.
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.
createClient<SanityValues>()(config)
Removing the double function signature from createClient
:
1- const client = createClient<SanityValues>()({ 2+ const client = createClient<SanityValues>({ 3 // ... 4});
We no longer derive types from your config values. Most of the types weren't significant, but the main loss will be _originalId
when the perspective
was "previewDrafts"
.
castFromTyped
Casting from typed to untyped is now just a simple cast:
1+ import type { SanityClient as SanityClientNative } from "@sanity/client"; 2 3- import { castFromTyped, createClient } from "@sanity-typed/client"; 4+ import { createClient } from "@sanity-typed/client"; 5 6import type { SanityValues } from "./sanity.config"; 7 8const client = createClient<SanityValues>()({ 9 // ... 10}); 11 12export const typedClient = client; 13 14- export const untypedClient = castFromTyped(client); 15+ export const untypedClient = client as SanityClientNative; 16 17export default untypedClient;
castToTyped
still exists.
No vulnerabilities found.
No security vulnerabilities found.