DEV Community

yossarian
yossarian

Posted on • Updated on • Originally published at catchts.com

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' | '%';
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'>> // []
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)