Composition of type validators in TypeScript

In this article, You can find some type validation techniques.

Let's start with simple function

Assume, our function argument should be always some CSS value. For example: 100px, 10rem, 50% etc ...

First of all we should check if value ends with some allowed measure units:

type Units = 'px' | 'rem' | '%';

Now, we should be able to split our measure units into two parts: number and unit

type Units = 'px' | 'rem' | '%';

type IsValidCSS<T extends string> = T extends `${number}${Units}` ? true : false;

type Result = IsValidCSS<'10px'> // true
type Result2 = IsValidCSS<'10p'> // false

Lets write generic validator:

type Units = 'px' | 'rem' | '%';

type IsValidCSS<T extends string> = T extends `${number}${Units}` ? true : false;

type Validator<T extends boolean> = T extends true ? [] : [never];

type Test = Validator<IsValidCSS<'10px'>> // []

Please, give me a minute, I will explain why we need an array as a return type from Validator

Let's try it

const foo = <T,>(arg: T, ...validation: Validator<IsValidCSS<T>>) => {}

foo('10px'); // error

Still does not work, because argument is infered to string instead of literal 10px.

In order to fix it, we should apply additional constraints to the generic type:

const foo = <T extends string>(arg: T, ...validation: Validator<IsValidCSS<T>>) => {}

foo('10px'); // ok
foo('10%'); // ok
foo('10p'); // error

Is it possible to apply several validators?

Assume, we are not allowed to use 99 in our CSS

type Units = 'px' | 'rem' | '%';

type IsValidCSS<T> = T extends `${number}${Units}` ? true : false;

type StringNumber<T extends number> = `${T}`;

type IsAllowedNumber<T> = 
  T extends `${infer Num}${Units}` 
  ? Num extends StringNumber<99> 
  ? false 
  : true 
  : false;

type Validator<T extends boolean> = 
  T extends true 
  ? [] 
  : ['Dear developer, please use valid CSS values'];

const foo = <T extends string>
  (
    arg: T,
    ...validation: [...Validator<IsValidCSS<T>>, ...Validator<IsAllowedNumber<T>>]
  ) => { }

foo('100px'); // ok
foo('99px'); // expected error

Each time, when Validator fails, it returns [never] and because we are using rest operator it evaluates to never.

So if Validator has failed, TS expects second argument which is never.

That's all.

17