valibot icon indicating copy to clipboard operation
valibot copied to clipboard

Cannot refine instance properties using `intersect` of `instance` and `object`

Open chengluyu opened this issue 5 months ago • 6 comments

In many validation libraries, I can refine instance properties by using the intersection (also called conjunction) of a class instance and an object schema. Valibot doens't support this.

import * as v from "valibot";

class Box<A> {
  constructor(public readonly content: A) {}
}

const Simple = v.intersect([
  v.instance(Box<number>),
  v.object({ content: v.number() }),
]);

console.log(v.is(Simple, new Box("box"))); // false
console.log(v.is(Simple, new Box(42))); // false

chengluyu avatar Jul 06 '25 14:07 chengluyu

Hi @chengluyu! I'm Dosu and I’m helping the valibot team.

Valibot doesn't support refining instance properties by intersecting v.instance and v.object schemas. This is because Valibot treats class instances and plain objects as distinct types—when you use v.intersect with v.instance(Box) and v.object({ content: v.number() }), the object schema's validation isn't applied to class instances, even if the instance has matching properties. This is a deliberate design choice for strict type separation, which differs from some other validation libraries that allow this pattern. You can see this discussed in issue #1227.

As a workaround, maintainers suggest using v.looseObject and extending its pipe, or restructuring your validation logic, but direct intersection of instance and object schemas for property refinement isn't currently supported. More details on intersections and their limitations are in the official guide.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

dosubot[bot] avatar Jul 06 '25 14:07 dosubot[bot]

[...] because Valibot treats class instances and plain objects as distinct types [...]

That doesn't sound reasonable, because checking Box alone using the object schema passes without any issues.

const Simpler = v.object({ content: v.number() });

console.log(v.is(Simpler, new Box("box"))); // false
console.log(v.is(Simpler, new Box(42))); // true

chengluyu avatar Jul 06 '25 14:07 chengluyu

Hey, I guess the problem is that a plain object will fail the validation of v.instance(Box<number>). What do you expect as an output in this case? A plain object with properties from Box and the object schema? Or are you looking for an OR condition. If so you can replace v.intersect(...) with v.union(...). Here more context: https://valibot.dev/guides/unions/

fabian-hiller avatar Jul 11 '25 00:07 fabian-hiller

I guess the problem is that a plain object will fail the validation of v.instance(Box<number>)

I haven't seen the author put a plain object as input for v.is.

What do you expect as an output in this case?

I guess the author expected the first comment's second false to be true.

I don't want union in my case.

const phoneNumberErrorSchema = v.intersect([
  v.instance(ClientError),
  v.object({
    body: v.object({
      phoneNumber: v.pipe(v.array(v.string()), v.minLength(1)),
    }),
  }),
])

I traced library code execution and found the following.

    if (value1 && value2 && value1.constructor === Object && value2.constructor === Object) {

I used ClientError type, so it failed to merge. I can't sure this is intended.

jeiea avatar Aug 19 '25 03:08 jeiea

Hey, I guess the problem is that a plain object will fail the validation of v.instance(Box<number>). What do you expect as an output in this case? A plain object with properties from Box and the object schema?

No, I’m not passing plain objects to v.instance(Box<number>).

Or are you looking for an OR condition. If so you can replace v.intersect(...) with v.union(...). Here more context: https://valibot.dev/guides/unions/

No, OR matches either an instance of Box or an object with the property content being a number.

I want to create an validation object matching an instance of Box with the property content being a number.

From the perspective of set theory, I want to match the intersection of the set of all Boxes and all objects having the property content set to any number.

chengluyu avatar Aug 23 '25 02:08 chengluyu

The intersect schema attempts to merge the output of both schemas, so it cannot be used in this case. I recommend using a pipe to check the additional properties of a specific class.

import * as v from "valibot";

class Box<A> {
  constructor(public readonly content: A) {}
}

const Schema = v.pipe(
  v.instance(Box<number>),
  v.check((input) => typeof input.content === 'number')
);

fabian-hiller avatar Aug 24 '25 15:08 fabian-hiller