@nrk/nrkno-sanity-typesafe-schemas
v1.3.5
Published
_This library underpins the [principles of nrkno-sanity](https://github.com/nrkno/nrkno-sanity-libs/blob/master/docs/nrkno-sanity-principles.md) and [option driven design](https://github.com/nrkno/nrkno-sanity-libs/blob/master/packages/sanity-plugin-nrkno
Downloads
69
Maintainers
Keywords
Readme
@nrk/nrkno-sanity-typesafe-schemas
This library underpins the principles of nrkno-sanity and option driven design.
Sanity Typesafe Schemas
Utility functions to ease Sanity schema development:
schema(typeName, schemaDefinition)
: used to define schemas that is consumed bycreateSchema
.field(typeName, fieldDefinition)
: used to define fields fordocument
,object
,file
andimage
schemas.arrayOf(typeName, arrayMemberDefinition)
: used to define of-entries inarray
schemas.typed<T>(anything)
: passthrough function to provide inline types in json objects.checkSchema<T>(anything)
: safeguard function that ensures an interface contains all field-names used in a schemaforType<T>(anything)
: utility function meant to be used withcheckSchema
API design goals
- Typesafe Sanity schemas (and with that, autocompletion)
- Feature discovery of built-in Sanity features (types, options, validations)
- Breadcrumb-trails to relevant Sanity docs
- Feature discovery & documentation of extension types & features (custom input-resolver options, reusable schema-types etc.)
- Decrease time from idea to finished schema for developers, by embracing the above.
- Opt-in. Ie, this can be sprinkled into an existing codebase to gradually add type safety to schemas.
- Minimal runtime footprint. Should not mess with the schema provided by the developer.
See the-gist-of-it-all.ts for a scaled down version of how the helper functions work.
Type discovery
Feature discovery
Typesafety
Documentation
Installation
npm install --save @nrk/nrkno-sanity-typesafe-schemas
or
yarn add @nrk/nrkno-sanity-typesafe-schemas
Usage
import { field, schema } from '@nrk/nrkno-sanity-typesafe-schemas';
export const typesafeDocumentSchema = schema('document', {
name: 'some-doc',
title: 'Some document',
fields: [
field('string', {
name: 'someField',
title: 'Some title',
initialValue: 'a',
options: {
list: [
{ value: 'a', title: 'A' },
{ value: 'b', title: 'B' },
],
layout: 'radio',
},
}),
],
});
//@ts-expect-error schema is not correctly defined
const withErrorSchema = schema('string', {});
See code examples for more.
Extending Sanity schemas
Use TypeScript declaration merging
to extend the types under @nrk/nrkno-sanity-typesafe-schemas
:
// document-extension.ts
declare module '@nrk/nrkno-sanity-typesafe-schemas' {
// adds field 'document' schemas
interface DocumentSchema {
custom?: boolean;
}
}
// number-extension.ts
import '@nrk/nrkno-sanity-typesafe-schemas';
declare module '@nrk/nrkno-sanity-typesafe-schemas' {
// adds the custom option to 'number' schemas (Number already has options, so we extend the NumberOption)
interface NumberOptions {
custom?: boolean;
}
}
// somewhere using typesafe helpers
export const typesafeDocumentSchema = schema('document', {
/* omitted */
custom: true, // custom is now a valid option for all document schemas
});
export const typesafeNumberSchema = schema('document', {
/* omitted */
options: {
custom: true, // custom is now a valid option for all number schemas
},
});
A list of all extendable types can be found in index.ts.
Adding reusable schema types
Define your schema type, and use TypeScript declaration merging
to extend SchemaDirectory
in @snorreeb/sanity-typesafe-schemas
:
// special-string-schema.ts
import { StringSchema } from '@nrk/nrkno-sanity-typesafe-schemas';
export type SpecialStringSchema = Omit<StringSchema, 'type'> & {
type: 'special-string';
options: {
special: string;
};
};
// schema-directory-extension.ts
import '@nrk/nrkno-sanity-typesafe-schemas';
declare module '@nrk/nrkno-sanity-typesafe-schemas' {
interface SchemaDirectory {
'special-string': SpecialStringSchema;
}
}
// somewhere using typesafe helpers
export const specialStringSchema = schema(
'special-string', // special-string is now a valid, autocompletable type
{
/* omitted */
}
);
Escape hatches
Defining ad-hoc types without extending anything
The ''
string is a general escape hatch that still provides some safety. undefined
and null
will also work.
Use alongside the typed
generic helper function, to define inline types.
import { schema, typed } from '@snorreeb/sanity-typesafe-schemas';
export const stringSchemaUsedByNameSomewhereElse = schema('', {
type: 'string', // when using '', type must be provided here
name: 'this-name-is-used-in-another-schema',
title: 'Custom stringtype for whatever reason',
inputComponent: () => 'Render me softly',
options: typed<{ customOption: boolean }>({
customOption: true,
}),
});
// ...
export const schemaUsingTheAbove = schema('', {
type: stringSchemaUsedByNameSomewhereElse.name,
title: 'New title',
name: 'type-pointing-to-another-type',
});
Force undefined
title
Title is optional in Sanity Studio, but will result in console.log warnings if omitted. Therefore, it is required by the schema-helpers.
To set an undefined title anyway, hack-cast it:
const mySchema = schema('object', {
title: undefined as unknown as string,
});
Runtime footprint
Compiled to javascript, the helper functions look like this:
function schema(type, schema) {
return { type, ...schema };
}
function field(type, schema) {
return { type, ...schema };
}
function arrayOf(type, schema) {
return { type, ...schema };
}
function typed(anything) {
return anything;
}
Ie, they are basically an identity functions with no overhead.
Bonus feature: checkSchema
It is often useful to have the stored type of a Sanity document available in the studio, so it can be used in custom components, hidden-functions, validation-context or similar.
A common problem then, is that it is easy to forget to update the interface after adding a field to schema. To rectify this slightly, the checkSchema helper-function can be used.
Example
import { schema, field, checkSchema, forType } from '@snorreeb/sanity-typesafe-schemas';
// as const narrows string to 'my-type' literal
const type = 'my-type' as const;
interface SchemaType {
_type: typeof type;
field1: string;
field2: string;
}
export const mySchema = schema('object', {
name: type,
fields: [
field('datetime', {
name: 'field1', // field automatically narrows name from string to 'field1' literal type
// intellisense for datetime-properties
}),
{
name: 'field2' as const, // need const here, to narrow type from string to 'field1' literal
type: 'string',
},
],
});
// this will fail to compile if a new field is added to mySchema without also adding it in SchemaType
// Note: it does NOT check that schema-field types correspond with interface types
checkSchema(forType<SchemaType>(), mySchema);
See src/examples/check-schema.ts for details.
FAQ
Why are Sanity schema types partially reimplemented?
Types in @sanity/types
are the runtime definition of schemas, obtained
after passing schema-definitions to createSchema
.
The types we want when defining schemas are subtly different and has more optionality and must allow for
extension. Some of the @sanity/types
ae also to wide to be useful for autocompletion.
Therefore, types are reimplemented to resemble the Sanity schema documentation, in most places.
Why not use Schema types directly?
It is tempting to think this "just work":
const docSchema: DocumentSchema = {
type: 'document',
name: 'my-schema',
title: 'My document',
fields: [
{
// autocomplete all the things :fingers-crossed:
},
],
};
This works fine for schemas. In fact, feel free to use the exported Schema-types in this repository like this if you like.
However, for fields, this becomes tricky, when a union type is used to describe all possible
field-types: fields: UnionOfSchemas[]
.
First, until the discriminator type-field is provided, the autocomplete-listing is overwhelming, showing all possible fields for all possible schemas.
Second, even with the discriminator type-field, autocomplete in IDE's becomes unreliable for options and other type-fields that are common across schemas (due to unintended, partial type merging). The helper functions circumvent this by using generics to do exact type-lookup for each schema.
Third, types cannot be used in js-codebases. The helper-functions can though, and will still provide intellisense in IDE's.
Fourth, a downside of using schema-types directly is that developers need to know the name of the schema. With the helper functions, simply starting an open string for type will list all available schemas with a registered type-definition.