@sanity/types
Version:
Type definitions for common Sanity data structures
313 lines (305 loc) • 10.3 kB
text/typescript
import {
type DefineArrayMemberBase,
type DefineSchemaBase,
type DefineSchemaOptions,
type MaybeAllowUnknownProps,
type NarrowPreview,
type StrictDefinition,
type WidenInitialValue,
type WidenValidation,
} from './defineTypes'
import {type FieldDefinitionBase, type IntrinsicTypeName} from './definition'
import {type AutocompleteString} from './types'
/**
* Helper function for defining a Sanity type definition. This function does not do anything on its own;
* it exists to check that your schema definition is correct, and help autocompletion in your IDE.
*
* This function will narrow the schema type down to fields and options based on the provided type-string.
*
* Schema types defined using `defineType` should typically be added to the Studio config under `schema.types`.
* Defined types can be referenced by their `name`. This is referred to as a type-alias.
*
* When using type-aliases as `type`, `defineType` cannot know the base-type, so type-safety will be reduced.
* If you know the base type of the type-alias, provide `defineOptions.aliasFor: <base type name>`.
* This will enforce that the schema definition conforms with the provided type.
*
* By default, `defineType` only allows known properties and options.
* Use `defineOptions.strict: false` to allow unknown properties and options.
*
* ### Basic usage
*
* ```ts
* defineType({
* type: 'object',
* name: 'custom-object',
* fields: [ {type: 'string', name: 'title', title: 'Title'}],
* })
* ```
*
* ### Usage with aliasFor narrowing
*
* ```ts
* defineType({
* type: 'custom-object',
* name: 'redefined-custom-object',
* options: {
* columns: 2
* }
* }, {aliasFor: 'object' })
* ```
*
* ### Allow unknown properties
*
* ```ts
* defineType({
* type: 'custom-object',
* name: 'redefined-custom-object',
* allowsUnknownProperties: true
* options: {
* columns: 2,
* allowsUnknownOptions: true
* }
* }, {strict: false})
* ```
* ### Maximum safety and best autocompletion
*
* Use {@link defineType}, {@link defineField} and {@link defineArrayMember}:
*
* ```ts
* defineType({
* type: 'object',
* name: 'custom-object',
* fields: [
* defineField({
* type: 'array',
* name: 'arrayField',
* title: 'Things',
* of: [
* defineArrayMember({
* type: 'object',
* name: 'type-name-in-array',
* fields: [defineField({type: 'string', name: 'title', title: 'Title'})],
* }),
* ],
* }),
* ],
* })
* ```
*
* ## Note on type-safety in the current implementation
*
* Type-safety inside array-like properties (schema properties like `fields` and `of`) can only be guaranteed when
* {@link defineField} and {@link defineArrayMember} are used to wrap each value in the array.
*
* For array-values without a function-wrapper, TypeScript will resolve to a union type of all possible properties across
* all schema types. This result in less precise typing.
*
* ### Extending the Sanity Schema types
*
* If you want to extend the Sanity Schema types with your own properties or options to make them typesafe,
* you can use [TypeScript declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html).
*
* With declaration merging, properties and options will be available in a type-safe manner, and
* `strict: false` will not be necessary.
*
* #### Example: Add option to StringOptions
*
* ```ts
* // string.ts
*
* //redeclare the sanity module
* declare module 'sanity' {
* // redeclare StringOptions; it will be merged with StringOptions in the sanity module
* export interface StringOptions {
* myCustomOption?: boolean
* }
* }
*
* // the option is now part of the StringOptions type, just as if it was declared in the sanity codebase:
* defineType({
* type: 'string',
* name: 'my-string',
* options: {
* myCustomOption: true // this does not give an error anymore
* }
* })
*
* ```
*
* #### Example: Add a schema definition to "intrinsic-types"
*
* ```ts
* //my-custom-type-definition.ts
*
* // create a new schema definition based on object (we remove the ability to assign field, change the type add some options)
* export type MagicallyAddedDefinition = Omit<Schema.ObjectDefinition, 'type' | 'fields'> & {
* type: 'magically-added-type'
* options?: {
* sparkles?: boolean
* }
* }
*
* // redeclares sanity module so we can add interfaces props to it
* declare module 'sanity' {
* // redeclares IntrinsicDefinitions and adds a named definition to it
* // it is important that the key is the same as the type in the definition ('magically-added-type')
* export interface IntrinsicDefinitions {
* 'magically-added-type': MagicallyAddedDefinition
* }
* }
*
* // defineType will now narrow `type: 'magically-added-type'` to `MagicallyAddedDefinition`
* defineType({
* type: 'magically-added-type'
* name: 'magic',
* options: {
* sparkles: true // this is allowed,
* //@ts-expect-error this is not allowed in MagicallyAddedDefinition.options
* sparks: true
* }
* })
* ```
*
* @param schemaDefinition - should be a valid schema type definition.
* @param defineOptions - optional param to provide type hints for `schemaDefinition`.
*
* @see defineField
* @see defineArrayMember
* @see typed
*
* @beta
*/
export function defineType<
const TType extends IntrinsicTypeName | AutocompleteString,
const TName extends string,
TSelect extends Record<string, string> | undefined,
TPrepareValue extends Record<keyof TSelect, any> | undefined,
TAlias extends IntrinsicTypeName | undefined,
TStrict extends StrictDefinition,
>(
schemaDefinition: {
type: TType
name: TName
} & DefineSchemaBase<TType, TAlias> &
NarrowPreview<TType, TAlias, TSelect, TPrepareValue> &
MaybeAllowUnknownProps<TStrict>,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
defineOptions?: DefineSchemaOptions<TStrict, TAlias>,
): typeof schemaDefinition {
return schemaDefinition
}
/**
* Define a field within a document, object, image or file definition `fields` array.
*
* This function will narrow the schema type down to fields and options based on the provided
* type-string.
*
* Using `defineField` is optional, but should provide improved autocompletion in your IDE, when building your schema.
* Field-properties like `validation` and `initialValue`will also be more specific.
*
* See {@link defineType} for more examples.
*
* @param schemaField - should be a valid field type definition.
* @param defineOptions - optional param to provide type hints for `schemaField`.
*
* @see defineField
* @see defineArrayMember
* @see typed
*
* @beta
*/
export function defineField<
const TType extends IntrinsicTypeName | AutocompleteString,
const TName extends string,
TSelect extends Record<string, string> | undefined,
TPrepareValue extends Record<keyof TSelect, any> | undefined,
TAlias extends IntrinsicTypeName | undefined,
TStrict extends StrictDefinition,
>(
schemaField: {
type: TType
name: TName
} & DefineSchemaBase<TType, TAlias> &
NarrowPreview<TType, TAlias, TSelect, TPrepareValue> &
MaybeAllowUnknownProps<TStrict> &
FieldDefinitionBase,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
defineOptions?: DefineSchemaOptions<TStrict, TAlias>,
): typeof schemaField & WidenValidation & WidenInitialValue {
// TODO: re-evaluate the need for this cast
return schemaField as typeof schemaField & WidenValidation & WidenInitialValue
}
/**
* Define an array item member type within an array definition `of`-array.
*
* This function will narrow the schema type down to fields and options based on the provided
* `type` string.
*
* Using `defineArrayMember` is optional, but should provide improved autocompletion in your IDE, when building your schema.
* Field properties like `validation` and `initialValue` will also be more specific.
*
* See {@link defineType} for example usage.
*
* @param arrayOfSchema - should be a valid `array.of` member definition.
* @param defineOptions - optional param to provide type hints for `arrayOfSchema`.
*
* @see defineType
* @see defineField
* @see typed
*
* @beta
*/
export function defineArrayMember<
const TType extends IntrinsicTypeName | AutocompleteString,
const TName extends string,
TSelect extends Record<string, string> | undefined,
TPrepareValue extends Record<keyof TSelect, any> | undefined,
TAlias extends IntrinsicTypeName | undefined,
TStrict extends StrictDefinition,
>(
arrayOfSchema: {
type: TType
/**
* When provided, `name` is used as `_type` for the array item when stored.
*
* Necessary when an array contains multiple entries with the same `type`, each with
* different configuration (title and initialValue for instance).
*/
name?: TName
} & DefineArrayMemberBase<TType, TAlias> &
NarrowPreview<TType, TAlias, TSelect, TPrepareValue> &
MaybeAllowUnknownProps<TStrict>,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
defineOptions?: DefineSchemaOptions<TStrict, TAlias>,
): typeof arrayOfSchema & WidenValidation & WidenInitialValue {
// TODO: re-evaluate the need for this cast
return arrayOfSchema as typeof arrayOfSchema & WidenValidation & WidenInitialValue
}
/**
* `typed` can be used to ensure that an object conforms to an exact interface.
*
* It can be useful when working with `defineType` and `defineField` on occasions where a wider type with
* custom options or properties is required.
*
* ## Example usage
* ```ts
* defineField({
* type: 'string',
* name: 'nestedField',
* options: typed<StringOptions & {myCustomOption: boolean}>({
* layout: 'radio',
* // allowed
* myCustomOption: true,
* //@ts-expect-error unknownProp is not part of StringOptions & {myCustomOption: boolean}
* unknownProp: 'not allowed in typed context',
* }),
* }),
* ```
*
* @param input - returned directly
*
* @internal
*/
export function typed<T>(input: T): T {
return input
}