A
arktype2w ago
Felix

How to validate all properties without bailing early?

I am using Arktype to validate forms. I would like to show all problems with a form. I am using unions to show some form elements conditionally.
export const OtherPropertyRow = type.merge(
{
address: 'string > 0',
value: 'number',
mortgage: 'number',
},
type({
includedHow: "'Party1'",
})
.or({
includedHow: "'Party2'",
})
.or({
includedHow: "'Other'",
arrangement: 'string',
}),
type({
purpose: "'InvestmentProperty'",
})
.or({
purpose: "'BachOrHolidayHouse'",
})
.or({
purpose: "'Other'",
otherPurpose: 'string',
}),
);
export const OtherPropertyRow = type.merge(
{
address: 'string > 0',
value: 'number',
mortgage: 'number',
},
type({
includedHow: "'Party1'",
})
.or({
includedHow: "'Party2'",
})
.or({
includedHow: "'Other'",
arrangement: 'string',
}),
type({
purpose: "'InvestmentProperty'",
})
.or({
purpose: "'BachOrHolidayHouse'",
})
.or({
purpose: "'Other'",
otherPurpose: 'string',
}),
);
that's a real world example of what i am building. the problem is when i don't have includedHow set, it will not report errors for address etc.
5 Replies
Felix
FelixOP2w ago
ssalbdivad
ssalbdivad2w ago
Generally it is pretty difficult to control exactly how union errors are reported. Note for example that in TypeScript, it will choose arbitrarily a single branch and a single problem to report, which may or may not be realistic or helpful. In ArkType, we try to discriminate the branches first, then report errors on the resulting branch, but generally you can imagine how convoluted it would be if we reported every possible error for every possible branch. If you need this level of granular control over your errors, your best bet is probably to do something like what we talked about before in terms of narrowing a wider object or write your own internal logic that will check the properties you want to determine which branch you're checking, then delegate to arktype to do the full validation on that branch.
Felix
FelixOP2w ago
Yes I understand, however, in this example every branch shares the top fields. Wouldn't it be possible to check all common fields without going down a branch? If that is not an option, what would a realistic example look like where I can still delegate most of the validation logic to arktype? here's an initial go at it, however, it won't validate e.g. arrangement or otherPurpose fields if there are validation errors in the left hand side of the pipe() call:
export const OtherPropertyRow = type({
address: 'string > 0',
value: 'number',
mortgage: 'number',
includedHow: '"Party1" | "Party2" | "Other"',
purpose: '"InvestmentProperty" | "BachOrHolidayHouse" | "Other"',
})
.pipe((o) => {
if (o.includedHow === 'Other') {
const s = type({
arrangement: 'string',
})(o);
if (s instanceof type.errors) return s;
return {
...o,
s,
};
}
return o;
})
.pipe((o) => {
if (o.purpose === 'Other') {
const s = type({
otherPurpose: 'string',
});
if (s instanceof type.errors) return s;
return {
...o,
s,
};
}
});
export const OtherPropertyRow = type({
address: 'string > 0',
value: 'number',
mortgage: 'number',
includedHow: '"Party1" | "Party2" | "Other"',
purpose: '"InvestmentProperty" | "BachOrHolidayHouse" | "Other"',
})
.pipe((o) => {
if (o.includedHow === 'Other') {
const s = type({
arrangement: 'string',
})(o);
if (s instanceof type.errors) return s;
return {
...o,
s,
};
}
return o;
})
.pipe((o) => {
if (o.purpose === 'Other') {
const s = type({
otherPurpose: 'string',
});
if (s instanceof type.errors) return s;
return {
...o,
s,
};
}
});
i could get it to work like this for now:
export const OtherPropertyRow = type({
includedHow: 'string',
purpose: 'string',
})
.partial()
.pipe((o) => {
return type({
address: 'string > 0',
value: 'number',
mortgage: 'number',
purpose: '"InvestmentProperty" | "BachOrHolidayHouse" | "Other"',
...(o.includedHow === 'Other'
? {
arrangement: 'string > 0',
}
: {}),
includedHow: '"Party1" | "Party2" | "Other"',
...(o.purpose === 'Other'
? {
otherPurpose: 'string > 0',
}
: {}),
})(o);
});
export const OtherPropertyRow = type({
includedHow: 'string',
purpose: 'string',
})
.partial()
.pipe((o) => {
return type({
address: 'string > 0',
value: 'number',
mortgage: 'number',
purpose: '"InvestmentProperty" | "BachOrHolidayHouse" | "Other"',
...(o.includedHow === 'Other'
? {
arrangement: 'string > 0',
}
: {}),
includedHow: '"Party1" | "Party2" | "Other"',
...(o.purpose === 'Other'
? {
otherPurpose: 'string > 0',
}
: {}),
})(o);
});
ssalbdivad
ssalbdivad2w ago
Yeah, something like this. The key is that pipe allows you to control what parts of the object get evaluated sequentially and where you want things to stop if one of the previous piped validations fails. The biggest recommendation I'd have is to: 1. Define the piped type separately so it doesn't get instantiated every time your object is validated. 2. Pipe to the type directly like .pipe(outputType) so you can avoid creating unnecessary wrappers. It also makes the output of the type introspectable at runtime. You can see an example of this kind of chained validation in the intro here: https://arktype.io/docs/intro/morphs-and-more
ArkType
ArkType Docs
TypeScript's 1:1 validator, optimized from editor to runtime
Felix
FelixOP2w ago
I am struggling to see how to apply the recommendations in my scenario because i have to validate different fields based on input. what am i missing?

Did you find this page helpful?