XOR (Exclusive OR) #343
Replies: 7 comments 8 replies
-
Thanks for starting this discussion. Can you provide a simple practical use case of XOR in a schema library. I am interested in investigating XOR to maybe include it in the library before we reach v1 in the next few weeks or months. |
Beta Was this translation helpful? Give feedback.
-
Here is a cut down use case for you. import * as v from "https://deno.land/x/[email protected]/mod.ts";
const BaseSource = v.object({
patches: v.optional(v.array(v.string())),
folder: v.optional(v.string()),
});
const LocalSource = v.intersect([
BaseSource,
v.object({
path: v.string(),
use_gitignore: v.optional(v.boolean(), true),
}),
]);
const UrlSource = v.intersect([
BaseSource,
v.object({
url: v.string(),
sha256: v.optional(v.string([v.regex(/[a-fA-F0-9]{64}/)])),
md5: v.optional(v.string([v.regex(/[a-fA-F0-9]{32}/)])),
}),
]);
const GitSource = v.intersect([
BaseSource,
v.object({
git_url: v.string([v.url()]),
git_rev: v.optional(v.string(), "HEAD"),
git_depth: v.optional(v.number([v.integer()])),
}),
]);
// Perhaps a new method v.xor here?
const Source = v.union([LocalSource, UrlSource, GitSource]);
// This would ideally throw an exception as it's not a valid combination
const result = v.parse(Source, {
url: "http://localhost",
git_url: "http://github.com",
});
if (v.is(UrlSource, result)) {
console.log(result.url);
}
if (v.is(GitSource, result)) {
console.log(result.git_url);
} FWIW: This is based on https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json |
Beta Was this translation helpful? Give feedback.
-
For now I think I'm just going to use |
Beta Was this translation helpful? Give feedback.
-
Also even with-in the first example I gave we have another case of 2 mutually exclusive properties. export const UrlSource = intersect([
Source,
object({
/**
* The url that points to the source.
* This should be an archive that is extracted in the working directory.
*/
url: string(),
/**
* The SHA256 hash of the source archive.
*/
sha256: optional(string()),
/**
* The MD5 hash of the source archive.
*/
md5: optional(string()),
}, [
custom(
(i) => [i.sha256, i.md5].filter((v) => typeof v !== "undefined" && v !== null).length === 1,
"sha256 & md5 are mutually exclusive, you must give one or the other but not both.",
),
]),
]); In this case I have solved it with a custom validation but an API like this would be neat. export const UrlSource = intersect([
Source,
object({
/**
* The url that points to the source.
* This should be an archive that is extracted in the working directory.
*/
url: string(),
/**
* The SHA256 hash of the source archive.
*/
sha256: requiredIf(_ => _.md5 === null, string()),
/**
* The MD5 hash of the source archive.
*/
md5: requiredIf(_ => _.sha256 === null, string()),
}),
]); How you go about making that type safe I have no idea. Perhaps some like this could use the type alegbra from https://github.com/maninak/ts-xor export const UrlSource = intersect([
Source,
object({
/**
* The url that points to the source.
* This should be an archive that is extracted in the working directory.
*/
url: string(),
/**
* The hash that the downloaded file must match.
*/
hash: xor([
object({sha256: string()}),
object({md5: string()}),
])
}),
]); |
Beta Was this translation helpful? Give feedback.
-
FWIW this pattern basically works. export const Source = union([LocalSource, HttpSource, GitSource], [
custom((i) => [LocalSource, HttpSource, GitSource].filter((_) => is(_, i)).length === 1),
]);
export type Source = XOR<Input<typeof LocalSource>, Input<typeof HttpSource>, Input<typeof GitSource>>; I haven't even looked at the valibot source code yet perhaps I might, I feel like this is pretty close to what I'm after. |
Beta Was this translation helpful? Give feedback.
-
Ok I got there in the end, trick is to add import type { XOR } from "npm:ts-xor";
import * as v from "https://deno.land/x/[email protected]/mod.ts";
const Foo = v.object({ value1: v.string() }, v.never());
const Bar = v.object({ value2: v.string() }, v.never());
export const FooBar = v.union([Foo, Bar]);
export type FooBar = v.Input<typeof FooBar>;
export type FooBarXOR = XOR<v.Input<typeof Foo>, v.Input<typeof Bar>>;
// So runtime validation is now working for the xor use case. Just needed to add `never()`
console.log("Parse Foo Succeeds", v.safeParse(FooBar, { value1: "abc" }).success === true);
console.log("Parse Bar Succeeds", v.safeParse(FooBar, { value2: "abc" }).success === true);
console.log("Parse Invalid Combination Throws", v.safeParse(FooBar, { value1: "abc", value2: "abc" }).success === false);
// However the TypeScript Type should disallow this
const x = { value1: "abc", value2: "abc" } as FooBar;
// Like this
const y = { value1: "abc", value2: "abc" } as FooBarXOR; |
Beta Was this translation helpful? Give feedback.
-
Can you provide me with sample code with a minimal schema or point me to the comment I should focus on? Unfortunately, I don't have the time to go through everything. |
Beta Was this translation helpful? Give feedback.
-
I have just found myself in another position where I am trying to model an existing schema with TypeScript, initially started with Zod, then swapped over to valibot. Nice lib. Anyway I need to represent some mutually exclusive types. Not just a handful of properties but entire types that don't share any properties. ie: We are not talking about a discriminated union here.
https://github.com/maninak/ts-xor works well for my needs but is obviously only a design time thing, would be amazing if valibot could work out how to incorporate similar functionality but at runtime.
Perhaps the switch API proposed here colinhacks/zod#2106 could help?
Would a normal union be the best I could do with valibot today?
Beta Was this translation helpful? Give feedback.
All reactions