Validating typed arrays
I use a lot of Uint8Array values in my objects. Is it possible to validate those?
Here's a reproducer:
import * as v from "@badrap/valita";
const BytesHolder = v.object({
fourBytes: v.array(v.number()).assert((val) => val.length === 4)
});
console.log('Primitive array');
BytesHolder.parse({
fourBytes: [1, 2, 3, 4],
});
console.log('Typed array');
BytesHolder.parse({
fourBytes: new Uint8Array([1, 2, 3, 4]),
});
The second call fails.
Yup, it's possible by passing a generic to the .assert() method:
import * as v from "@badrap/valita";
const BytesHolder = v.object({
fourBytes: v
.unknown()
.assert<Uint8Array>((value) => value instanceof Uint8Array && value.length === 4)
});
// Passes
BytesHolder.parse({
fourBytes: new Uint8Array([1, 2, 3, 4])
});
You can also use .chain() if you need more custom control over error messages in custom validators as an alternative to .assert().
Oh, nice! I guess this whole v.unknown().assert<Uint8Array>((value) => value instanceof Uint8Array && value.length === 4) chain could be packaged into a uint8ArrayOfLength(number) helper function to make it more ergonomic, right?
Or is there even a way to extend v with new methods from user code?
I wonder whether it would make sense to have a generic v.instanceOf<T>(...) method provided by valita for these cases. I think a signature like this would work: v.instanceOf<T>(constructor: {new (): T}) which would expand to v.assert<T>((value) => value instanceof constructor). It could be used like this: v.instanceOf<Uint8Array>(Uint8Array).
I like to spread the gospel of composable helper functions, as I believe that we should keep Valita's own API surface relatively small and provide a solid base for crafting these kinds of building blocks:
function instanceOf<T>(t: abstract new (...args: any[]) => T): v.Type<T> {
return v
.unknown()
.assert<T>(
(value) => value instanceof t,
`expected an instance of ${t.name || "an anonymous type"}`
);
}
This function can then be imported whenever needed and used (and reused) for all kinds of shenanigans:
const t = v.object({
fourBytes: instanceOf(Uint8Array).assert((a) => a.length === 4).optional(),
timestamp: instanceOf(Date).optional(),
etcetera: instanceOf(Worker).optional(),
});
t.parse({ timestamp: "Hello, World!" });
// ValitaError: custom_error at .timestamp (expected an instance of Date)
That's not to say some kind of v.instanceOf in the future is totally out of question. In fact, that instanceOf implementation is almost identical to one we use at work, for stuff like instanceOf(Date) and instanceOf(Buffer). But for now I recommend the above approach. It can be super-powerful! 🙂
As random side note: if you need a primitive array with a fixed length, then you can use tuple:
// this has the type v.Type<[string, number]>
v.tuple([v.string(), v.number()]);
Cool! Maybe an example like that could be added to the README, under a section like "Custom Helper Functions"?
In any case, from my side the helper function approach works fine, so this issue can probably be closed. Thanks for your quick help!
Whoops, forgot to close this in 2021. Well, better late than never I guess 😅