sindresorhus / ow

Function argument validation for humans
https://sindresorhus.com/ow/
MIT License
3.8k stars 104 forks source link

Q: how to make custom type narrowing validator (i.e. Predicate<x>)? #241

Open leaumar opened 2 years ago

leaumar commented 2 years ago

Colleagues and I have been wanting to make validators to assert values to be of specific subtypes, e.g. a union of specific strings, and narrow the type accordingly. We've been going over the docs here for that but it just isn't clear to us, sorry. We feel the meaning of most of the internal ow type names (Predicate, BasePredicate, Validator, ReusableValidator, Main, etc) is a little lost on us and this doesn't help us make sense of the relevant API.

type AB = 'a' | 'b';
const data = getData();
ow(data, ow.object.partialShape({
  letter: ow.string.oneOf(['a', 'b'])
}));
data.letter; // string, but we want AB

We've been trying various angles but the types never "click". Given the usual quality/DX of your libraries, we would expect there's some super simple factory function to call with a validator and generic type param, but we can't seem to cobble that together. The closest we've come is finding these APIs:

  function isAB(value: unknown): value is AB {...}

  class ABPredicate extends Predicate<AB> {
    constructor() {
      super('string');
      this.addValidator(ow.create<AB>(isAB));
    }
  }

  const ab: BasePredicate<AB> = {
    [testSymbol]: ow.string.is(isAB),
  };

The goal being:

const data: unknown;
ow(data, ow.object.partialShape({ letter: ouchAB }));
data.letter; // AB, not just string

Sorry if it's a stupid question but we'd like to stop fumbling around with it and just ask a more seasoned user or maintainer. How do we properly (e.g. a helpful failure message isn't even considered yet in our examples so far, but it should be done) and concisely (as little as possible boilerplate) make a validator to assert values and narrow the asserted type?

leaumar commented 2 years ago

The best we now have is this

  class ABPredicate extends Predicate<AB> {
    constructor() {
      super('string');
      this.addValidator({ validator: isAB, message: (value, label) => 'TODO' });
    }
  }

and it does fulfill our purpose from what we can tell. At least the value's type is narrowed to AB after the ow() call.

But this way, the validator function isAB is given a value to assert that's already cast by ow to AB instead of the string or unknown that we'd expect, suggesting that this is either an ow API weakness or this implementation does something backward.

sindresorhus commented 2 years ago

ow.create is meant for creating custom validators. I don't remember whether it would solve your needs though. I also don't have time to actively maintain this package anymore (meaning, I'm happy to accept PRs, but cannot commit time to new features), so you may be better off finding a different package with a more active maintainer.

leaumar commented 2 years ago

So I gather from you bringing that up that what we want would be a new feature, it's not really that we're missing something. Was afraid of that... I might put in the time to make a pr, I do love this lib compared to e.g. joi.