DEV Community

Fran González
Fran González

Posted on

Validum, my take on fluent validation using Conditional Types

Recently I started working on a side-project in TypeScript in which I had to create a huge amount of forms and validations for them. Coming from a C# background I always loved the FluentValidation library that allows devs to use a DSL to create validations instead of just putting a thousand ifs in their code.

public class CustomerValidator : AbstractValidator<Customer> {
  public CustomerValidator() {
    RuleFor(x => x.Surname).NotEmpty();
    RuleFor(x => x.Forename).NotEmpty().WithMessage("Please specify a first name");
    RuleFor(x => x.Discount).NotEqual(0).When(x => x.HasDiscount);
    RuleFor(x => x.Address).Length(20, 250);
    RuleFor(x => x.Postcode).Must(BeAValidPostcode).WithMessage("Please specify a valid postcode");
  }

  private bool BeAValidPostcode(string postcode) {
    // custom postcode validating logic goes here
  }
}

Wonderful! Now, there's a couple of alternatives for TypeScript, but I usually don’t like using classes when I’m working with TypeScript unless they’re strictly necessary. And so it came to me: what if I create a validation library that allows to use a DSL-like language without needing to create a class? Because who likes finishing the project they had in mind when they can start creating a new one just for it, right?

And so I began to explore the API that I'd eventually create, something like this:

const user = {
    name: 'Test McTestFace',
    username: 'test',
    age: 17,
}

const result = validation
    .of(user)
    .property('name')
        .alphanumeric()
        .maxLength(10)
        .withMessage('Who has a name that long?')
        .withCode('ERR-100')
    .andProperty('age')
        .greaterThanOrEqual(18)
        .withMessage('Only adults allowed!')
    .result()

The basic idea was to expose a single method (of) that depending on the input that was given it would return a validator for the given type plus a bunch of utility methods that would allow the user to attach more information to the validation (withMessage, withCode, etc.), apply the validation conditionally (when, unless, etc.) and other validations that apply to all types.

So with the help of the wonderful article Advanced Types in TypeScript I began to explore the different options I had to implement this. My initial idea was to create different interfaces that would be returned conditionally depending on the given input; turns out this is completely possible with the use of Conditional Types!

Conditional types were introduced in TypeScript 2.8 and allows to specify a type like this:

T extends U ? X : Y

This means: if T extends U then return the type X, otherwise return Y. It's hard to see the usefulness of this until you see a real example, so let's take a look at how I used this in Validum. Remember that of method I mentioned before? This is how it's defined:

of<T, K extends keyof T>(
        input: T,
        propertyName?: K,
    ): Validator<T>

(Have you ever seen K extends key of T syntax? It means that the type K only allows keys of the type T, so the parameter propertyName can only be keys of the object T)

And the Validator type is defined as follows:

type Validator<T> = string extends T
    ? StringValidator<T>
    : any[] extends T
    ? CollectionValidator<T>
    : [] extends T
    ? CollectionValidator<T>
    : number extends T
    ? NumberValidator<T>
    : Date extends T
    ? DateValidator<T>
    : ObjectValidator<T>

(If you see the full source code you'll notice that the real types include another type P which is necessary for chaining validations, but I trimmed it out in this example)

A lot to unpack in here! What the Validator<T> type is doing is returning a matching interface based on the type T, so if it's a subclass of string it will return the StringValidator<T>, otherwise it'll check if it's a subclass of an array and return the CollectionValidator<T> and so on and so forth.

This is incredibly helpful for the users of the library since there's no way they can use a method designed for other types, and also helps with the autocompletion:

VSCode autocompletion

So all that was left was to create the interfaces, their implementation and a little bit of code to parse the expressions and evaluate them. The project is completely open source and it's available on GitHub. I've published a couple of versions already and I have been testing it in my personal project. I personally love the syntax. What about you? Do you know any other alternatives to create validations?

Feel free to contribute if you feel there's anything missing or if you want to tackle any of the open issues :)

Oldest comments (0)