valita icon indicating copy to clipboard operation
valita copied to clipboard

Validating typed arrays

Open threema-danilo opened this issue 4 years ago • 4 comments

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.

threema-danilo avatar Dec 01 '21 18:12 threema-danilo

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().

marvinhagemeister avatar Dec 01 '21 19:12 marvinhagemeister

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).

threema-danilo avatar Dec 01 '21 20:12 threema-danilo

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()]); 

jviide avatar Dec 02 '21 02:12 jviide

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!

threema-danilo avatar Dec 02 '21 10:12 threema-danilo

Whoops, forgot to close this in 2021. Well, better late than never I guess 😅

jviide avatar Feb 19 '23 23:02 jviide