πŸ“„ File detail

utils/semanticNumber.ts

🧩 .tsπŸ“ 37 linesπŸ’Ύ 1,486 bytesπŸ“ text
← Back to All Files

🎯 Use case

This file lives under β€œutils/”, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, …). On the API surface it exposes semanticNumber β€” mainly functions, hooks, or classes. Dependencies touch schema validation.

Generated from folder role, exports, dependency roots, and inline comments β€” not hand-reviewed for every path.

🧠 Inline summary

import { z } from 'zod/v4' /** * Number that also accepts numeric string literals like "30", "-5", "3.14". *

πŸ“€ Exports (heuristic)

  • semanticNumber

πŸ“š External import roots

Package roots from from "…" (relative paths omitted).

  • zod

πŸ–₯️ Source preview

import { z } from 'zod/v4'

/**
 * Number that also accepts numeric string literals like "30", "-5", "3.14".
 *
 * Tool inputs arrive as model-generated JSON. The model occasionally quotes
 * numbers β€” `"head_limit":"30"` instead of `"head_limit":30` β€” and z.number()
 * rejects that with a type error. z.coerce.number() is the wrong fix: it
 * accepts values like "" or null by converting them via JS Number(), masking
 * bugs rather than surfacing them.
 *
 * Only strings that are valid decimal number literals (matching /^-?\d+(\.\d+)?$/)
 * are coerced. Anything else passes through and is rejected by the inner schema.
 *
 * z.preprocess emits {"type":"number"} to the API schema, so the model is
 * still told this is a number β€” the string tolerance is invisible client-side
 * coercion, not an advertised input shape.
 *
 * .optional()/.default() go INSIDE (on the inner schema), not chained after:
 * chaining them onto ZodPipe widens z.output<> to unknown in Zod v4.
 *
 *   semanticNumber()                              β†’ number
 *   semanticNumber(z.number().optional())         β†’ number | undefined
 *   semanticNumber(z.number().default(0))         β†’ number
 */
export function semanticNumber<T extends z.ZodType>(
  inner: T = z.number() as unknown as T,
) {
  return z.preprocess((v: unknown) => {
    if (typeof v === 'string' && /^-?\d+(\.\d+)?$/.test(v)) {
      const n = Number(v)
      if (Number.isFinite(n)) return n
    }
    return v
  }, inner)
}