Hyperlinkv0.8.0-beta.28

SchemaRepresentation

SchemaRepresentation.toCodeDocumentfunctioneffect/SchemaRepresentation.ts:2369
(
  multiDocument: MultiDocument,
  options?: { readonly reviver?: Reviver<Code> | undefined }
): CodeDocument

Generates TypeScript code strings from a MultiDocument.

When to use

Use when you need to produce source code for Effect Schema definitions from a schema representation MultiDocument.

Details

options.reviver can customize code generation for Declaration nodes. Return undefined to fall back to the default logic, which uses generation annotations or the encoded schema. References are topologically sorted so non-recursive definitions are emitted before their dependents. $ref keys are converted to sanitized JavaScript identifiers.

Example (Generating TypeScript code)

import { Schema, SchemaRepresentation } from "effect"

const Person = Schema.Struct({
  name: Schema.String,
  age: Schema.Int
})

const multi = SchemaRepresentation.toMultiDocument(
  SchemaRepresentation.fromAST(Person.ast)
)
const codeDoc = SchemaRepresentation.toCodeDocument(multi)
console.log(codeDoc.codes[0].runtime)
// Schema.Struct({ ... })
export function toCodeDocument(multiDocument: MultiDocument, options?: {
  /**
   * The reviver can return `undefined` to indicate that the generation should be generated by the default logic
   */
  readonly reviver?: Reviver<Code> | undefined
}): CodeDocument {
  const artifacts: Array<Artifact> = []

  const ts = topologicalSort(multiDocument.references)

  // Phase 1: Build sanitization map with collision handling
  const sanitizedReferenceMap = new Map<string, string>()
  const uniqueSanitizedReferences = new Set<string>()
  const referenceCount = new Map<string, number>()

  // Process all references first to build the map
  const allRefs = [
    ...ts.nonRecursives.map(({ $ref }) => $ref),
    ...Object.keys(ts.recursives)
  ]

  for (const ref of allRefs) {
    ensureUniqueSanitized(ref)
  }

  // Phase 2: Use the map when processing references
  const nonRecursives = ts.nonRecursives.map(({ $ref, representation }) => ({
    $ref: sanitizedReferenceMap.get($ref)!,
    code: recur(representation)
  }))
  const recursives = Rec.mapEntries(ts.recursives, (representation, $ref) => [
    sanitizedReferenceMap.get($ref)!,
    recur(representation)
  ])

  const codes = multiDocument.representations.map(recur)

  return {
    codes,
    references: {
      nonRecursives: nonRecursives.filter(({ $ref }) => (referenceCount.get($ref) ?? 0) > 0),
      recursives: Rec.filter(recursives, (_, $ref) => (referenceCount.get($ref) ?? 0) > 0)
    },
    artifacts
  }

  function ensureUniqueSanitized(originalRef: string): string {
    // Check if already mapped (consistency)
    const sanitized = sanitizedReferenceMap.get(originalRef)
    if (sanitized !== undefined) {
      return sanitized
    }

    // Find unique sanitized name
    const seed = sanitizeJavaScriptIdentifier(originalRef)
    let candidate = seed
    let suffix = 0

    while (uniqueSanitizedReferences.has(candidate)) {
      candidate = `${seed}${++suffix}`
    }

    uniqueSanitizedReferences.add(candidate)
    sanitizedReferenceMap.set(originalRef, candidate)
    return candidate
  }

  function addSymbol(s: symbol): string {
    const identifier = ensureUniqueSanitized("_symbol")
    const key = globalThis.Symbol.keyFor(s)
    const description = s.description
    const generation = key === undefined
      ? makeCode(`Symbol(${description === undefined ? "" : format(description)})`, `typeof ${identifier}`)
      : makeCode(`Symbol.for(${format(key)})`, `typeof ${identifier}`)
    artifacts.push({ _tag: "Symbol", identifier, generation })
    return identifier
  }

  function addEnum(s: Enum): string {
    const identifier = ensureUniqueSanitized("_Enum")
    artifacts.push({
      _tag: "Enum",
      identifier,
      generation: makeCode(
        `enum ${identifier} { ${s.enums.map(([name, value]) => `${format(name)}: ${format(value)}`).join(", ")} }`,
        `typeof ${identifier}`
      )
    })
    return identifier
  }

  function addImport(importDeclaration: string) {
    if (!artifacts.some((a) => a._tag === "Import" && a.importDeclaration === importDeclaration)) {
      artifacts.push({ _tag: "Import", importDeclaration })
    }
  }

  function recur(s: Representation): Code {
    const g = on(s)
    switch (s._tag) {
      default:
        return makeCode(
          g.runtime + toRuntimeAnnotate(s.annotations) + toRuntimeBrand(s.annotations),
          g.Type + toTypeBrand(s.annotations)
        )
      case "Reference":
        return g
      case "Declaration":
      case "String":
      case "Number":
      case "BigInt":
      case "Arrays":
      case "Objects":
      case "Suspend":
        return makeCode(
          g.runtime + toRuntimeAnnotate(s.annotations) + toRuntimeBrand(s.annotations) + toRuntimeChecks(s.checks),
          g.Type + toTypeBrand(s.annotations) + toTypeChecks(s.checks)
        )
    }
  }

  function on(s: Representation): Code {
    switch (s._tag) {
      case "Declaration": {
        // if there is a reviver, use it to generate the generation
        if (options?.reviver !== undefined) {
          // the reviver can return `undefined` to indicate that the generation should be generated by the default logic
          const out = options.reviver(s, recur)
          if (out !== undefined) {
            return out
          }
        }
        // otherwise, use the generation from the annotations
        const generation = s.annotations?.generation
        if (
          Predicate.isObject(generation) && typeof generation.runtime === "string" &&
          typeof generation.Type === "string"
        ) {
          const typeParameters = s.typeParameters.map(recur)
          if (typeof generation.importDeclaration === "string") {
            addImport(generation.importDeclaration)
          }
          return makeCode(
            replacePlaceholders(generation.runtime, typeParameters.map((p) => p.runtime)),
            replacePlaceholders(generation.Type, typeParameters.map((p) => p.Type))
          )
        }
        // otherwise, use the generation from the encoded schema
        return recur(s.encodedSchema)
      }
      case "Reference": {
        const sanitized = ensureUniqueSanitized(s.$ref)
        referenceCount.set(sanitized, (referenceCount.get(sanitized) ?? 0) + 1)
        return makeCode(sanitized, sanitized)
      }
      case "Suspend": {
        const thunk = recur(s.thunk)
        return makeCode(
          `Schema.suspend((): Schema.Codec<${thunk.Type}> => ${thunk.runtime})`,
          thunk.Type
        )
      }
      case "Null":
        return makeCode(`Schema.Null`, "null")
      case "Undefined":
        return makeCode(`Schema.Undefined`, "undefined")
      case "Void":
        return makeCode(`Schema.Void`, "void")
      case "Never":
        return makeCode(`Schema.Never`, "never")
      case "Unknown":
        return makeCode(`Schema.Unknown`, "unknown")
      case "Any":
        return makeCode(`Schema.Any`, "any")
      case "Number":
        return makeCode(`Schema.Number`, "number")
      case "Boolean":
        return makeCode(`Schema.Boolean`, "boolean")
      case "BigInt":
        return makeCode(`Schema.BigInt`, "bigint")
      case "Symbol":
        return makeCode(`Schema.Symbol`, "symbol")
      case "String": {
        const contentMediaType = s.contentMediaType
        const contentSchema = s.contentSchema
        if (contentMediaType === "application/json" && contentSchema !== undefined) {
          return makeCode(`Schema.fromJsonString(${recur(contentSchema)})`, "string")
        } else {
          return makeCode(`Schema.String`, "string")
        }
      }
      case "Literal": {
        const literal = format(s.literal)
        return makeCode(`Schema.Literal(${literal})`, literal)
      }
      case "UniqueSymbol": {
        const identifier = addSymbol(s.symbol)
        return makeCode(`Schema.UniqueSymbol(${identifier})`, `typeof ${identifier}`)
      }
      case "ObjectKeyword":
        return makeCode(`Schema.ObjectKeyword`, "object")
      case "Enum": {
        const identifier = addEnum(s)
        return makeCode(`Schema.Enum(${identifier})`, `typeof ${identifier}`)
      }
      case "TemplateLiteral": {
        const parts = s.parts.map(recur)
        const type = toTypeParts(s.parts).map((p) => "`" + p + "`").join(" | ")
        return makeCode(`Schema.TemplateLiteral([${parts.map((p) => p.runtime).join(", ")}])`, type)
      }
      case "Arrays": {
        const elements = s.elements.map((e) => {
          return {
            isOptional: e.isOptional,
            type: recur(e.type),
            annotations: e.annotations
          }
        })

        const rest = s.rest.map(recur)

        if (Arr.isArrayNonEmpty(rest)) {
          const item = rest[0]
          if (elements.length === 0 && rest.length === 1) {
            return makeCode(
              `Schema.Array(${item.runtime})`,
              `ReadonlyArray<${item.Type}>`
            )
          }
          const post = rest.slice(1)
          return makeCode(
            `Schema.TupleWithRest(Schema.Tuple([${
              elements.map((e) =>
                toRuntimeIsOptional(e.isOptional, e.type.runtime) + toRuntimeAnnotateKey(e.annotations)
              ).join(", ")
            }]), [${rest.map((r) => r.runtime).join(", ")}])`,
            `readonly [${
              elements.map((e) => toTypeIsOptional(e.isOptional, e.type.Type)).join(", ")
            }, ...Array<${item.Type}>${post.length > 0 ? `, ${post.map((p) => p.Type).join(", ")}` : ""}]`
          )
        }
        return makeCode(
          `Schema.Tuple([${
            elements.map((e) => toRuntimeIsOptional(e.isOptional, e.type.runtime) + toRuntimeAnnotateKey(e.annotations))
              .join(", ")
          }])`,
          `readonly [${elements.map((e) => toTypeIsOptional(e.isOptional, e.type.Type)).join(", ")}]`
        )
      }
      case "Objects": {
        const pss = s.propertySignatures.map((p) => {
          const isSymbol = typeof p.name === "symbol"
          const name = isSymbol ? addSymbol(p.name) : formatPropertyKey(p.name)
          const nameType = toTypeIsOptional(
            p.isOptional,
            toTypeIsMutable(p.isMutable, isSymbol ? `[typeof ${name}]` : name)
          )
          const type = recur(p.type)
          return makeCode(
            `${isSymbol ? `[${name}]` : name}: ${
              toRuntimeIsOptional(p.isOptional, toRuntimeIsMutable(p.isMutable, type.runtime))
            }` +
              toRuntimeAnnotateKey(p.annotations),
            `${nameType}: ${type.Type}`
          )
        })

        const iss = s.indexSignatures.map((is) => {
          return {
            parameter: recur(is.parameter),
            type: recur(is.type)
          }
        })

        if (iss.length === 0) {
          // 1) Only properties -> Struct
          return makeCode(
            `Schema.Struct({ ${pss.map((p) => p.runtime).join(", ")} })`,
            `{ ${pss.map((p) => p.Type).join(", ")} }`
          )
        } else if (pss.length === 0 && iss.length === 1) {
          // 2) Only one index signature and no properties -> Record
          return makeCode(
            `Schema.Record(${iss[0].parameter.runtime}, ${iss[0].type.runtime})`,
            `{ readonly [x: ${iss[0].parameter.Type}]: ${iss[0].type.Type} }`
          )
        } else {
          // 3) Properties + index signatures -> StructWithRest
          return makeCode(
            `Schema.StructWithRest(Schema.Struct({ ${pss.map((p) => p.runtime).join(", ")} }), [${
              iss.map((is) => `Schema.Record(${is.parameter.runtime}, ${is.type.runtime})`).join(", ")
            }])`,
            `{ ${pss.map((p) => p.Type).join(", ")}, ${
              iss.map((is) => `readonly [x: ${is.parameter.Type}]: ${is.type.Type}`).join(", ")
            } }`
          )
        }
      }
      case "Union": {
        if (s.types.length === 0) {
          return makeCode("Schema.Never", "never")
        }
        if (s.types.every((t) => t._tag === "Literal")) {
          const literals = s.types.map((l) => format(l.literal))
          if (literals.length === 1) {
            return makeCode(`Schema.Literal(${literals[0]})`, literals[0])
          }
          return makeCode(`Schema.Literals([${literals.join(", ")}])`, literals.join(" | "))
        }
        const mode = s.mode === "anyOf" ? "" : `, { mode: "oneOf" }`
        const types = s.types.map((t) => recur(t))
        return makeCode(
          `Schema.Union([${types.map((t) => t.runtime).join(", ")}]${mode})`,
          types.map((t) => t.Type).join(" | ")
        )
      }
    }
  }

  function toTypeBrand(annotations: Schema.Annotations.Annotations | undefined): string {
    const brands = collectBrands(annotations)
    if (brands.length === 0) return ""
    addImport(`import type * as Brand from "effect/Brand"`)
    return brands.map((b) => ` & Brand.Brand<${format(b)}>`).join("")
  }

  function toTypeChecks(checks: ReadonlyArray<Check<Meta>>): string {
    return checks.map((c) => toTypeCheck(c)).join("")
  }

  function toTypeCheck(check: Check<Meta>): string {
    switch (check._tag) {
      case "Filter":
        return toTypeBrand(check.annotations)
      case "FilterGroup": {
        return toTypeChecks(check.checks)
      }
    }
  }

  function toRuntimeChecks(checks: ReadonlyArray<Check<Meta>>): string {
    return checks.map((c) => `.check(${toRuntimeCheck(c)})` + toRuntimeBrand(c.annotations)).join("")
  }

  function toRuntimeCheck(check: Check<Meta>): string {
    switch (check._tag) {
      case "Filter":
        return toRuntimeFilter(check)
      case "FilterGroup": {
        const a = toRuntimeAnnotations(check.annotations)
        const ca = a === "" ? "" : `, ${a}`
        return `Schema.makeFilterGroup([${check.checks.map((c) => toRuntimeCheck(c)).join(", ")}]${ca})`
      }
    }
  }

  function toRuntimeFilter(filter: Filter<Meta>): string {
    const a = toRuntimeAnnotations(filter.annotations)
    const ca = a === "" ? "" : `, ${a}`
    switch (filter.meta._tag) {
      case "isTrimmed":
      case "isGUID":
      case "isULID":
      case "isBase64":
      case "isBase64Url":
      case "isUppercased":
      case "isLowercased":
      case "isCapitalized":
      case "isUncapitalized":
      case "isFinite":
      case "isInt":
      case "isUnique":
      case "isDateValid":
        return `Schema.${filter.meta._tag}(${a})`

      case "isStringFinite":
      case "isStringBigInt":
      case "isStringSymbol":
      case "isPattern":
        return `Schema.${filter.meta._tag}(${toRuntimeRegExp(filter.meta.regExp)}${ca})`

      case "isMinLength":
        return `Schema.isMinLength(${filter.meta.minLength}${ca})`
      case "isMaxLength":
        return `Schema.isMaxLength(${filter.meta.maxLength}${ca})`
      case "isLengthBetween":
        return `Schema.isLengthBetween(${filter.meta.minimum}, ${filter.meta.maximum}${ca})`
      case "isUUID":
        return `Schema.isUUID(${filter.meta.version}${ca})`
      case "isStartsWith":
        return `Schema.isStartsWith(${format(filter.meta.startsWith)}${ca})`
      case "isEndsWith":
        return `Schema.isEndsWith(${format(filter.meta.endsWith)}${ca})`
      case "isIncludes":
        return `Schema.isIncludes(${format(filter.meta.includes)}${ca})`

      case "isGreaterThan":
      case "isGreaterThanBigInt":
      case "isGreaterThanDate":
        return `Schema.${filter.meta._tag}(${toRuntimeValue(filter.meta.exclusiveMinimum)}${ca})`
      case "isGreaterThanOrEqualTo":
      case "isGreaterThanOrEqualToBigInt":
      case "isGreaterThanOrEqualToDate":
        return `Schema.${filter.meta._tag}(${toRuntimeValue(filter.meta.minimum)}${ca})`
      case "isLessThan":
      case "isLessThanBigInt":
      case "isLessThanDate":
        return `Schema.${filter.meta._tag}(${toRuntimeValue(filter.meta.exclusiveMaximum)}${ca})`
      case "isLessThanOrEqualTo":
      case "isLessThanOrEqualToBigInt":
      case "isLessThanOrEqualToDate":
        return `Schema.${filter.meta._tag}(${toRuntimeValue(filter.meta.maximum)}${ca})`
      case "isBetween":
      case "isBetweenBigInt":
      case "isBetweenDate":
        return `Schema.${filter.meta._tag}({ minimum: ${toRuntimeValue(filter.meta.minimum)}, maximum: ${
          toRuntimeValue(filter.meta.maximum)
        }, exclusiveMinimum: ${toRuntimeValue(filter.meta.exclusiveMinimum)}, exclusiveMaximum: ${
          toRuntimeValue(filter.meta.exclusiveMaximum)
        }${ca})`

      case "isMultipleOf":
        return `Schema.isMultipleOf(${filter.meta.divisor}${ca})`

      case "isMinProperties":
        return `Schema.isMinProperties(${filter.meta.minProperties}${ca})`
      case "isMaxProperties":
        return `Schema.isMaxProperties(${filter.meta.maxProperties}${ca})`
      case "isPropertiesLengthBetween":
        return `Schema.isPropertiesLengthBetween(${filter.meta.minimum}, ${filter.meta.maximum}${ca})`
      case "isPropertyNames":
        return `Schema.isPropertyNames(${recur(filter.meta.propertyNames).runtime}${ca})`

      case "isMinSize":
        return `Schema.isMinSize(${filter.meta.minSize}${ca})`
      case "isMaxSize":
        return `Schema.isMaxSize(${filter.meta.maxSize}${ca})`
      case "isSizeBetween":
        return `Schema.isSizeBetween(${filter.meta.minimum}, ${filter.meta.maximum}${ca})`
    }
  }
}