richardscarrott / ok-computer

λ "Functions all the way down" data validation for JavaScript and TypeScript.
MIT License
79 stars 0 forks source link

Type guard using `okay(validator, value)` #12

Closed richardscarrott closed 2 years ago

richardscarrott commented 2 years ago
import { object, string, okay } from 'ok-computer';
const validator = object({ name: string });
const value: unknown = 'foo';
if (okay(validator, value)) {
   type A = typeof value; // { name: string; }
} else {
   type B = typeof value; // unknown
}
import { and, array, string, minLength, okay } from 'ok-computer';
const value = [1, 2, 3];
if (okay(string, value)) {
  console.log(value.toLowerCase()); // AG
}

New

import { assert, and, string, minLength, instanceOf } from 'ok-computer';
import invariant from 'tiny-invariant';

// Previously, I wouldn't bother with ok-computer for simple validations as it was cumbersome, e.g.
const fn = (foo, bar, baz, now) => {
   invariant(typeof foo !== 'string', 'Expected string');
   invariant(typeof bar !== 'string', 'Expected string');
   invariant(typeof baz !== 'string' && baz.length > 10, 'Expected string with length > 10');
   invariant(now instanceof Date, 'Expected Date');
   // do stuff
}

// But with the new `okay` and `assert` functions, it's way more viable and potentially neater, especially if the validation logic get's more complex, e.g.
const fn = (foo, bar, baz, now) => {
  assert(string, foo);
  assert(string, bar);
  assert(and(string, minLength(10)), baz);
  assert(instanceOf(Date), now);
  // do stuff
}

// Although using `okay` to infer a narrowed type would be problematic as `and` just takes the inference of the first validator? 🤔

import { okay, string } from 'ok-computer';

const fn2 = (foo: string | number) => {
  if (typeof foo === 'string' && foo.length >= 10) {
    return foo.toUpperCase();
  }
  return foo.toExponential(); // correctly errors...
};

const fn3 = (foo: string | number) => {
  if (okay(and(string, minLength(10)), foo)) {
    return foo.toUpperCase();
  }
  return foo.toExponential(); // should error...
};

TODO

Improvement

If you use the errors outside of the type guard, you end up running the validator twice, e.g.

const validator = object({ name: string });

type Values = Infer<typeof validator>;

interface Props {
   onSubmit: (values: Values) => void;
}

const Form: React.FunctionalComponent<Props> = ({ onSubmit }) => {
  const [values, setValues] = useState<Record<keyof Values, unknown>>();
  const errors = validator(values); // once
  return <form onSubmit={() => {
     if (!okay(validator, values)) { // twice (used to be `if (hasError(errors)) {`)
       return;
     }
     onSubmit(values);
  }}>
     <input name="firstName" onChange={(e) => setValues(() => ({ ...values, [e.target.name]: e.target.value })))} />
     {errors.name ? <p>errors.name</p> : null}</>
</form>
}

TBH I'm not sure I care that much as it's reasonably unlikely to be a problem (performance wise) and callers can just use isError and cast (as Values) or memoize if they're really concerned?