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
- Add a distinguishing required property to one branch so dispatch is deterministic.
- Make the union ordered if your usage supports it (e.g. pipe with explicit precedence) instead of an unordered `or`.
- Restrict one branch's input (narrow its props/types) so inputs no longer overlap.
- 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
- Design union branches with a required discriminator key (kind/type/version).
- Prefer ordered parse attempts over unordered `or` when morphs are involved.
- Add a build-time check that renders every union to catch indeterminate cases early.
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
- Invalid pattern: {err}
- transparent (brush_parser::BindingParseError)
- Destination option ${key} must be a string
- Destination option ${key} must be a finite number
- Destination option ${key} must be a boolean
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/0c856b98ba38cefd.
Report an issue: GitHub.