Ilja
Ilja

Reputation: 46527

Creating type safe, chained validation

I'm at the stage where I need to perform several chained validations on some data, thus far I've been doing following:

// validation.ts

function isData(data: any) {
  return Boolean(data);
}

function isString(data: any) {
  return typeof data === "string";
}

function isLength(data: any, length: number) {
  return data?.length === length;
}

I would then call these one by one, but it gets a bit tedious. Ideally I am trying to come up with chained API like in example below, that takes in data and validates it (while returning correct type) with each chained funtion:

validate(data).isData().isString().isLength(3)

Or something of similar sorts. I am stuck at figuring out how to make such chain-able api, while also piping data in a type safe manner to each validator i.e. in isString data should already not be undefined.

Upvotes: 2

Views: 715

Answers (3)

Abdelmonaem Shahat
Abdelmonaem Shahat

Reputation: 544

try this:

class Validator {
   constructor(private data:string) {
      this.data = data;
    }
   isNotEmpty() {
      if (this.data) {
         return this;
      } else {
         throw Error('empty value')
      }
   }
    isString() {
        if (typeof(this.data) === 'string') {
            return this;
        } else {
            throw Error('[ValueError] Not a string')
        }
    }

    isLength(len: number) {
        if (this.data.length === len) {
            return this;
        } else {
            throw Error('[ValueError] Not of length ' + len)
        }
    }
}
const validate = new Validator("");
console.log(validate.isNotEmpty());

Upvotes: 0

bugs
bugs

Reputation: 15323

A different approach is to have some sort of primitive monadic validation chain, where each function in the chain performs a validation and return either an error (if the validation failed) or the argument that's been passed (if the validation succeeded).

You can then compose those functions together to have your total validation.

Note that this simple implementation destroys the information of exactly which individual check failed, if you want to preserve that you need to use actual Monads :)

type Validation<A> = "Invalid" | A

function isData<D>(data: D): Validation<D>  {
   return Boolean(data) ? data : "Invalid"
}

function isString<D>(data: D): Validation<D>  {
  return typeof data === "string" ? data : "Invalid"
}

function isLength(data: any, length: number): Validation<{ length: number }>  {
  return data?.length === length ? data : "Invalid"
}

declare function pipe<A, B, C, D>(a: A, f1: (a: A) => B, f2: (b: B) => C, f3: (c: C) => D): D
declare function pipe<A, B, C>(a: A, f1: (a: A) => B, f2: (b: B) => C): C
declare function pipe<A, B>(a: A, f1: (a: A) => B): B

const validate = <D>(d: D) => pipe(
    d,
    isData,
    isString,
    s => isLength(s, 3)
)

Playground link

Upvotes: 0

Vishnudev Krishnadas
Vishnudev Krishnadas

Reputation: 10970

Create a type validator class and have functions return class scope. A prominent usage is seen in the Builder Pattern.

Code Link: TypeScript Play

class TypeValidator {
    data: any;
    constructor(data: any) {
        this.data = data;
    }

    isString() {
        if (typeof(this.data) === 'string') {
            return this;
        } else {
            throw Error('[ValueError] Not a string')
        }
    }

    isLength(len: number) {
        if (this.data.length === len) {
            return this;
        } else {
            throw Error('[ValueError] Not of length ' + len)
        }
    }
}

const tv = new TypeValidator('test')
console.log(tv.isString().isLength(4));

Test 1

tv.isString().isLength(3);

VM30:19 Uncaught Error: [ValueError] Not of length 3
    at TypeValidator.isLength (eval at <anonymous> (main-3.js:1239), <anonymous>:19:19)
    at eval (eval at <anonymous> (main-3.js:1239), <anonymous>:24:27)
    at main-3.js:1239

Test 2

const tv = new TypeValidator(['abx'])
tv.isString();

VM31:11 Uncaught Error: [ValueError] Not a string
    at TypeValidator.isString (eval at <anonymous> (main-3.js:1239), <anonymous>:11:19)
    at eval (eval at <anonymous> (main-3.js:1239), <anonymous>:24:16)
    at main-3.js:1239

Upvotes: 3

Related Questions