import { hasDomain, isThunk, omit, printable, throwParseError } from "@ark/util"; import { intrinsic } from "../intrinsic.js"; import { compileSerializedValue } from "../shared/compile.js"; import { ArkErrors } from "../shared/errors.js"; import { defaultValueSerializer, implementNode } from "../shared/implement.js"; import { registeredReference } from "../shared/registry.js"; import { traverseKey } from "../shared/traversal.js"; import { BaseProp, intersectProps } from "./prop.js"; const implementation = implementNode({ kind: "optional", hasAssociatedError: false, intersectionIsOpen: true, keys: { key: {}, value: { child: true, parse: (schema, ctx) => ctx.$.parseSchema(schema) }, default: { preserveUndefined: true } }, normalize: schema => schema, reduce: (inner, $) => { if ($.resolvedConfig.exactOptionalPropertyTypes === false) { if (!inner.value.allows(undefined)) { return $.node("optional", { ...inner, value: inner.value.or(intrinsic.undefined) }, { prereduced: true }); } } }, defaults: { description: node => `${node.compiledKey}?: ${node.value.description}` }, intersections: { optional: intersectProps } }); export class OptionalNode extends BaseProp { constructor(...args) { super(...args); if ("default" in this.inner) assertDefaultValueAssignability(this.value, this.inner.default, this.key); } get rawIn() { const baseIn = super.rawIn; if (!this.hasDefault()) return baseIn; return this.$.node("optional", omit(baseIn.inner, { default: true }), { prereduced: true }); } get outProp() { if (!this.hasDefault()) return this; const { default: defaultValue, ...requiredInner } = this.inner; return this.cacheGetter("outProp", this.$.node("required", requiredInner, { prereduced: true })); } expression = this.hasDefault() ? `${this.compiledKey}: ${this.value.expression} = ${printable(this.inner.default)}` : `${this.compiledKey}?: ${this.value.expression}`; defaultValueMorph = getDefaultableMorph(this); defaultValueMorphRef = this.defaultValueMorph && registeredReference(this.defaultValueMorph); } export const Optional = { implementation, Node: OptionalNode }; const defaultableMorphCache = {}; const getDefaultableMorph = (node) => { if (!node.hasDefault()) return; const cacheKey = `{${node.compiledKey}: ${node.value.id} = ${defaultValueSerializer(node.default)}}`; return (defaultableMorphCache[cacheKey] ??= computeDefaultValueMorph(node.key, node.value, node.default)); }; export const computeDefaultValueMorph = (key, value, defaultInput) => { if (typeof defaultInput === "function") { // if the value has a morph, pipe context through it return value.includesTransform ? (data, ctx) => { traverseKey(key, () => value((data[key] = defaultInput()), ctx), ctx); return data; } : data => { data[key] = defaultInput(); return data; }; } // non-functional defaults can be safely cached as long as the morph is // guaranteed to be pure and the output is primitive const precomputedMorphedDefault = value.includesTransform ? value.assert(defaultInput) : defaultInput; return hasDomain(precomputedMorphedDefault, "object") ? // the type signature only allows this if the value was morphed (data, ctx) => { traverseKey(key, () => value((data[key] = defaultInput), ctx), ctx); return data; } : data => { data[key] = precomputedMorphedDefault; return data; }; }; export const assertDefaultValueAssignability = (node, value, key) => { const wrapped = isThunk(value); if (hasDomain(value, "object") && !wrapped) throwParseError(writeNonPrimitiveNonFunctionDefaultValueMessage(key)); // if the node has a default value, finalize it and apply JIT optimizations // if applicable to ensure behavior + error logging is externally consistent // (using .in here insead of .rawIn triggers finalization) const out = node.in(wrapped ? value() : value); if (out instanceof ArkErrors) { if (key === null) { // e.g. "Default must be assignable to number (was string)" throwParseError(`Default ${out.summary}`); } const atPath = out.transform(e => e.transform(input => ({ ...input, prefixPath: [key] }))); // e.g. "Default for bar must be assignable to number (was string)" // e.g. "Default for value at [0] must be assignable to number (was string)" throwParseError(`Default for ${atPath.summary}`); } return value; }; export const writeNonPrimitiveNonFunctionDefaultValueMessage = (key) => { const keyDescription = key === null ? "" : typeof key === "number" ? `for value at [${key}] ` : `for ${compileSerializedValue(key)} `; return `Non-primitive default ${keyDescription}must be specified as a function like () => ({my: 'object'})`; };