typescriptzod

Property 'shape' does not exist on type in Zod?


I am doing this in Zod:

export const LoadSort: z.ZodType<LoadSort> = z.object({
  name: z.string(),
  tilt: z.enum(['+', '-']),
})

console.log(LoadSort.shape)

But getting an error on .shape:

Property 'shape' does not exist on type 'ZodType<LoadSort, ZodTypeDef, LoadSort>'.ts(2339)

Actually, my code is more complex and requires me to use non-inferred types. Here is my full code

import { z } from 'zod'

export const LOAD_FIND_TEST = [
  'bond',
  'base_link_mark',
  'head_link_mark',
  'base_mark',
  'head_mark',
  'base_text',
  'miss_bond',
  'have_bond',
  'have_text',
] as const

export type Load = {
  find?: LoadFind
  read?: LoadRead
  save?: LoadSave
  task?: string
}

export type LoadFind = LoadFindLink | Array<LoadFindLink>

export type LoadFindBind = {
  form: 'bind'
  list: Array<LoadFindLink>
}

export type LoadFindLike = {
  base: LoadFindLikeLinkBond
  form: 'like'
  head: LoadFindLikeBond | LoadFindLikeLinkBond
  test: LoadFindTest
}

export type LoadFindLikeBond = string | boolean | null | number

export type LoadFindLikeLinkBond = {
  link: string
}

export type LoadFindLink = LoadFindLike | LoadFindRoll | LoadFindBind

export type LoadFindRoll = {
  form: 'roll'
  list: Array<LoadFindLink>
}

export type LoadFindTest = (typeof LOAD_FIND_TEST)[number]

export type LoadRead = {
  [key: string]: true | LoadReadLink
}

export type LoadReadLink = {
  find?: LoadFind
  read: LoadRead
}

export type LoadSave = {
  [key: string]: Array<LoadSaveBase> | LoadSaveBase
}

export type LoadSaveBase = {
  find?: LoadFind
  read?: LoadRead
  save?: LoadSave
  task?: string
}

export type LoadSort = {
  name: string
  tilt: '+' | '-'
}

export const Load: z.ZodType<Load> = z.object({
  find: z.optional(z.lazy(() => LoadFind)),
  read: z.optional(z.lazy(() => LoadRead)),
  save: z.optional(z.lazy(() => LoadSave)),
  task: z.optional(z.string()),
})

export const LoadFind: z.ZodType<LoadFind> = z.union([
  z.lazy(() => LoadFindLink),
  z.array(z.lazy(() => LoadFindLink)),
])

export const LoadRead: z.ZodType<LoadRead> = z.record(
  z.union([z.lazy(() => LoadReadLink), z.literal(true)]),
)

export const LoadSave: z.ZodType<LoadSave> = z.record(
  z.union(
    z.array(z.lazy(() => LoadSaveBase)),
    z.lazy(() => LoadSaveBase),
  ),
)

export const LoadFindBind: z.ZodType<LoadFindBind> = z.object({
  form: z.literal('bind'),
  list: z.array(z.lazy(() => LoadFindLink)),
})

export const LoadFindRoll: z.ZodType<LoadFindRoll> = z.object({
  form: z.literal('roll'),
  list: z.lazy(() => z.array(LoadFindLink)),
})

export const LoadFindTest = z.enum([
  'bond',
  'base_link_mark',
  'head_link_mark',
  'base_mark',
  'head_mark',
  'base_text',
  'miss_bond',
  'have_bond',
  'have_text',
])

export const LoadFindLike: z.ZodType<LoadFindLike> = z.object({
  base: z.lazy(() => LoadFindLikeLinkBond),
  form: z.literal('like'),
  head: z.union([
    z.lazy(() => LoadFindLikeLinkBond),
    z.lazy(() => LoadFindLikeBond),
  ]),
  test: LoadFindTest,
})

export const LoadFindLink: z.ZodType<LoadFindLink> = z.union([
  z.lazy(() => LoadFindLike),
  z.lazy(() => LoadFindRoll),
  z.lazy(() => LoadFindBind),
])

export const LoadFindLikeBond: z.ZodType<LoadFindLikeBond> = z.union([
  z.string(),
  z.boolean(),
  z.null(),
  z.number(),
])

export const LoadFindLikeLinkBond: z.ZodType<LoadFindLikeLinkBond> =
  z.object({
    link: z.string(),
  })

export const LoadReadLink: z.ZodType<LoadReadLink> = z.object({
  find: z.optional(LoadFind),
  read: LoadRead,
})

export const LoadSaveBase: z.ZodType<LoadSaveBase> = z.object({
  find: z.optional(LoadFind),
  read: z.optional(LoadRead),
  save: z.optional(LoadSave),
  task: z.optional(z.string()),
})

export const LoadSort: z.ZodType<LoadSort> = z.object({
  name: z.string(),
  tilt: z.enum(['+', '-']),
})

How do I both specify the ZodType and at the same time still get access to all the properties like .shape that come on the inferred object?

I am able to sort of get it working with this hack, but it doesn't seem right:

export const LoadSort: z.ZodType<LoadSort> = z.object({
  name: z.string(),
  tilt: z.enum(['+', '-']),
})

assertZodObject(LoadSort)

for (const name in LoadSort.shape) {
  const def = LoadSort.shape[name] as z.ZodType
  console.log(def)
}

export function assertZodObject<S extends z.ZodObject<z.ZodRawShape>>(
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  x: any,
): asserts x is S {
  if (!(x instanceof z.ZodObject)) {
    throw new Error()
  }
}

Solution

  • Because you indicate that LoadSort(the variable) is z.ZodType<LoadSort>, and .shape property is exist in ZodObject, not ZodType.

    To access .shape property and keep schema type checking, try to use satisfies keyword:

    const LoadSort = z.object({
      name: z.string(),
      tilt: z.enum(['+', '-']),
    }) satisfies z.ZodType<LoadSort>
    

    unrelated: don't use the same name on variable and interface, it may be confused.