can1357/oh-my-pi · error · OmpTypeError

ParseError: An unordered union of a type including a morph a

Error message

ParseError: An unordered union of a type including a morph and a type with overlapping input is indeterminate:\nLeft: ${leftExpression}\nRight: ${rightExpression}

What it means

When unioning a type containing a morph (input-transforming function) with another object type whose input overlaps, omptype cannot decide which branch to try first at parse time, making the union indeterminate. The error shows both branches rewritten with `, + (undeclared): delete` to expose their effective input types. Adding a required shared key on the non-morph branch is what disambiguates, via the `sharedRequired` check.

Source

Thrown at packages/omptype/src/type.ts:979

		return inheritScope(this, makeType(distributeFilter(this.ir, parseDef(def, this.resolver), false), [], {}));
	},

	onUndeclaredKey(this: InternalType, behavior: "ignore" | "reject" | "delete"): InternalType {
		const extras = behavior === "ignore" ? "keep" : behavior;
		const ir = withShallowExtras(this.ir, extras);
		if (extras === "delete" && ir.k === "union") {
			const objects = ir.members.filter((member): member is ObjectIR => member.k === "object");
			for (let left = 0; left < objects.length; left++) {
				for (let right = left + 1; right < objects.length; right++) {
					const sharedRequired = objects[left].props.some(
						leftProp =>
							!leftProp.opt &&
							objects[right].props.some(rightProp => !rightProp.opt && rightProp.key === leftProp.key),
					);
					if (!sharedRequired) {
						const leftExpression = expressionOf(objects[left]).replace(/ }$/, ", + (undeclared): delete }");
						const rightExpression = expressionOf(objects[right]).replace(/ }$/, ", + (undeclared): delete }");
						throw new OmpTypeError(
							`ParseError: An unordered union of a type including a morph and a type with overlapping input is indeterminate:\nLeft: ${leftExpression}\nRight: ${rightExpression}`,
						);
					}
				}
			}
		}
		return makeType(ir, this[kSteps], metaOf(this));
	},

	onDeepUndeclaredKey(this: InternalType, behavior: "ignore" | "reject" | "delete"): InternalType {
		return makeType(withDeepExtras(this.ir, behavior === "ignore" ? "keep" : behavior), this[kSteps], metaOf(this));
	},

	allows(this: InternalType, data: unknown): data is unknown {
		const steps = this[kSteps];
		let needsPredicates = false;
		for (const step of steps) {
			if (step.kind !== "pipe") {

View on GitHub (pinned to 9690622007)

Solutions

  1. Add a distinguishing required property to one branch so dispatch is deterministic.
  2. Make the union ordered if your usage supports it (e.g. pipe with explicit precedence) instead of an unordered `or`.
  3. Restrict one branch's input (narrow its props/types) so inputs no longer overlap.
  4. Handle the two shapes in separate parse attempts with explicit fallback logic.

Example fix

// before
type({ id: 'string.parse' }).or({ id: 'string', kind: 'string' }); // indeterminate

// after
// 'kind' is required on the right branch, disambiguating dispatch
type({ id: 'string.parse' }).or({ id: 'string', kind: 'string' });
// or narrow the morph branch input so they don't overlap
Defensive patterns

Strategy: validation

Validate before calling

// ensure at least one required key differentiates the branches before unioning
function branchesDisjoint(a, b) {
  const aKeys = Object.keys(a.shape ?? {});
  const bKeys = Object.keys(b.shape ?? {});
  return aKeys.some(k => !bKeys.includes(k)) || bKeys.some(k => !aKeys.includes(k));
}

Try / catch

try {
  const U = morphType.or(plainType);
} catch (e) {
  if (e instanceof OmpTypeError && e.message.includes('indeterminate')) {
    // fall back to sequential attempts: morphType.run first, then plainType.run
  } else throw e;
}

Prevention

When it happens

Trigger: `type(morphType).or(objectType)` (or `type([a, b])`) where both sides accept the same input shape — e.g. `type('string.numeric.parse').or('number')`-style overlap at the object level, or two objects both accepting `{a: string}` where one has a morph and no distinguishing required key.

Common situations: Versioned payload parsing (`v1|v2` where shapes overlap); unioning a coerced/transformed object with a plain object schema; migrating schemas so a previously distinct branch became overlapping.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/0c856b98ba38cefd. Report an issue: GitHub.