can1357/oh-my-pi · error · OmpTypeError

intersection of distinct morphs is indeterminate

Error message

intersection of distinct morphs is indeterminate

What it means

Intersecting two morphs (types with in/out transformation functions, e.g. string coerced to number) is only defined when both sides are the same morph with the same output. Different functions or outputs would make the resulting input/output ordering ambiguous, so omptype refuses with 'indeterminate' rather than picking an arbitrary order.

Source

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

			k: "alias",
			name: a.k === "alias" ? a.name : b.k === "alias" ? b.name : "intersection",
			resolve: () =>
				(resolved ??= intersectResolved(a.k === "alias" ? a.resolve() : a, b.k === "alias" ? b.resolve() : b)),
		};
		cache.set(b, reference);
		return reference;
	}
	return intersectResolved(a, b);
}

function intersectResolved(a: IR, b: IR): IR {
	if (a.k === "never" || b.k === "never") throw new OmpTypeError("intersection with never is unsatisfiable");
	if (a.k === "unknown") return b;
	if (b.k === "unknown") return a;
	if (a === b) return a;
	if (a.k === "morph" && b.k === "morph") {
		if (a.fn !== b.fn || a.out !== b.out) {
			throw new OmpTypeError("intersection of distinct morphs is indeterminate");
		}
		return { ...a, input: intersect(a.input, b.input) };
	}
	if (a.k === "morph") return { ...a, input: intersect(a.input, b) };
	if (b.k === "morph") return { ...b, input: intersect(a, b.input) };
	if (a.k === "sub" && a.schema.hasSteps) {
		if (b.k === "sub" && b.schema.hasSteps) {
			if (a.schema === b.schema) return a;
			throw new OmpTypeError("intersection of distinct morphs is indeterminate");
		}
		const schema = a.schema as InternalType;
		return embed(makeType(intersect(schema.ir, b), schema[kSteps], metaOf(schema)));
	}
	if (b.k === "sub" && b.schema.hasSteps) return intersect(b, a);
	if (a.k === "union" || b.k === "union") {
		const union = a.k === "union" ? a : b.k === "union" ? b : undefined;
		if (union === undefined) throw new OmpTypeError("union intersection invariant failed");
		const branches = union.members;

View on GitHub (pinned to 9690622007)

Solutions

  1. Use the identical morph instance/function on both sides of the intersection
  2. Pre-compose: apply one morph then refine with a plain predicate instead of intersecting two morphs
  3. Refactor so only one side performs the transformation and the other is a refinement of the output type
  4. If merging schemas (e.g. config + overrides), strip one side's morph before intersecting

Example fix

// before
intersect(number(string), number(coerceBigint))
// after
number(string).refine(n => Number.isSafeInteger(n))
Defensive patterns

Strategy: try-catch

Validate before calling

function morphsCompatible(a, b) {
  return a.ir?.k !== 'morph' || b.ir?.k !== 'morph' || (a.ir.fn === b.ir.fn && a.ir.out === b.ir.out);
}

Type guard

function isMorphSchema(t): boolean {
  return t != null && t.ir?.k === 'morph';
}

Try / catch

try {
  return intersect(a, b);
} catch (err) {
  if (err instanceof OmpTypeError && err.message.includes('morphs is indeterminate')) {
    throw new Error('two different morphs on one field — pick a single transformation');
  }
  throw err;
}

Prevention

When it happens

Trigger: intersect(number(string), number(bigintString)) or intersect(morph(f), morph(g)) with f !== g; intersecting two 'sub' schemas backed by different step chains also lands here.

Common situations: Combining two independently defined coercion schemas (e.g. two different string→number parsers) in an allOf/intersection; library defaults plus user overrides both installing a morph on the same field.

Related errors


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