TypeScript static validation
Tags: javascript, typescript, tutorial
Published: 2022-02-03 08:47:34

I assume you are familiar with TypeScript [mapped types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) and [type inference](https://www.typescriptlang.org/docs/handbook/type-inference.html).

In this article I will try to show you the power of static validation in TypeScript.

## Validation of inferred function arguments

Let's start from a small example to better understand the approach. Imagine we have a function which expects some css `width` value. It may be `100px`, `50vh` or `10ch`. Our function should do anything with argument, because we are not interested in business logic.
The naive approach would be to write this:
```typescript
const units = (value: string) => { }

units('hello!') // no error
```
Ofcourse, this is not what we want. Our function should allow only valid css value, it means that the argument should match the pattern `${number}${unit}`. Which in turn means that we need to create extra types. Let's try another one approach, more advanced:
```typescript
type CssUnits = 'px' | 'vh' | '%'

const units = (value: `${number}${CssUnits}`) => { }

units('20px') // ok
units('40') // error
units('40pxx') // error
```
Above solution looks good. Sorry, I'm not an expert in CSS units, this is all that I know :). Please be aware that unions inside template literal strings are distributive. It means that both `CssValue0` and `CssValue1` are equal. More about distributive types you can find [here](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types).

```typescript
type CssValue0 = `${number}${CssUnits}`;
type CssValue1 = `${number}px` | `${number}vh` | `${number}%`; 
```

Now we can extend our requirements. What if we are no longer allowed to use `%` units. Let me clarify. We are allowed to use all other css units. So you should treat this rule as a negation. Please be aware that there is no `negation` operator in typescript. For instance we are not allowed to declare a standalone type where `Data` might be any type but not an `"px"`. 

```typescript
type Data = not "px";
```

However, we can emulate this with help of inference on function arguments.

```typescript
type CssUnits = 'px' | 'vh' | '%'

type CssValue = `${number}${CssUnits}`

type ForbidPx<T extends CssValue> = T extends `${number}px` ? never : T

const units = <Value extends CssValue>(value: ForbidPx<Value>) => { }

units('40%') // ok
units('40vh') // ok
units('40px') // error
```
As you might have noticed, there were intoroduced several important changes. First of all, I have created `CssValue` type which represents our css value. Second, I have added `Value` generic argument in order to infer provided argument. Third, I have added `ForbidPx` utility type which checks if a provided generic argument contains `px`. If you are struggling to understand template literal syntax, please check [docs](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html).

`ForbidPx` might be represented through this js code:
```typescript
const IsRound = (str: string) => str.endsWith('px') ? null : str
```

Our types are still readable - it means we have not finished yet :). What would you say if we will add another one rule ? Let's say our client wants us to use only round numbers, like `100`, `50`, `10` and not `132`, `99`, `54`. Not a problem.
```typescript
type CssUnits = 'px' | 'vh' | '%'

type CssValue = `${number}${CssUnits}`

type ForbidPx<T extends CssValue> = T extends `${number}px` ? never : T
type IsRound<T extends CssValue> = T extends `${number}0${CssUnits}` ? T : never;


const units = <Value extends CssValue>(value: ForbidPx<Value> & IsRound<Value>) => { }

units('40%') // ok
units('401vh') // error, because we are allowed to use only rounded numbers
units('40px') // error, because px is forbidden
```
`IsRound` checks if there is `0` between the first part of css value and the last part (`CssUnits`). If there is `0`, this utility type returns `never`, otherwise it returns the provided argument.

You just intersect two filters and it is done. For the sake of brevity, let's get rid of all our validators and go back to our original implementation.
```typescript
type CssUnits = 'px' | 'vh' | '%'

type CssValue = `${number}${CssUnits}`

const units = <Value extends CssValue>(value: Value) => { }

```
Here is our new requirement. We should allow only numbers in range from `0` to `100`. This requirement is a tricky one, because TS does not support any range formats of `number` types. However, TypeScript does support recursion. It means that we can create a union of numbers. For instance `0 | 1 | 2 | 3 .. 100`. Before we do that, I will show you JavaScript representation of our algorithm:
```typescript
const range = (N: number, Result: 0[] = []): 0[] => {
  if (N === Result.length) {
    return Result
  }

  return range(N, [...Result, Result.length])
}
console.log(range(5)) // [0, 0, 0, 0, 0] 
```
I'd be willing to bet that this code is readable enough and self explanatory. Until length of `Result` is less than `N` we call `range` recursively with extra `zero`.

Let's see our implementation.
```typescript
type CssUnits = 'px' | 'vh' | '%'

type CssValue = `${number}${CssUnits}`

type MAXIMUM_ALLOWED_BOUNDARY = 101

type ComputeRange<
    N extends number,
    Result extends Array<unknown> = [],
    > =
    /**
     * Check if length of Result is equal to N
     */
    (Result['length'] extends N
        /**
         * If it is equal to N - return Result
         */
        ? Result
        /**
         * Otherwise call ComputeRange recursively with updated version of Result
         */
        : ComputeRange<N, [...Result, Result['length']]>
    )

type NumberRange = ComputeRange<MAXIMUM_ALLOWED_BOUNDARY>[number]

type IsInRange<T extends CssValue> =
    /**
     * If T extends CssValue type
     */
    T extends `${infer Num}${CssUnits}`
    /**
     * and Num extends stringified union of NumberRange
     */
    ? Num extends `${NumberRange}`
    /**
     * allow using T
     */
    ? T
    /**
     * otherwise - return never
     */
    : never
    : never

const units = <Value extends CssValue>(value: IsInRange<Value>) => { }

units('100px')
units('101px') // expected error
```
Implementation of `ComputeRange` is pretty straightforward. The only limit - is TypeScript internal limits of recursion.

Maximum value of `MAXIMUM_ALLOWED_BOUNDARY` which is supported by TypeScript is - `999`. It means that we can create a function which can validate RGB color format or IP address.

Because this article is published on `css-tricks.com`, I think it will be fair to validate `RGB`.

So, imagine you have a function which expects three arguments `R`, `G` and `B` accordingly.
```typescript
type MAXIMUM_ALLOWED_BOUNDARY = 256

type ComputeRange<
    N extends number,
    Result extends Array<unknown> = [],
    > =
    (Result['length'] extends N
        ? Result
        : ComputeRange<N, [...Result, Result['length']]>
    )

type U8 = ComputeRange<MAXIMUM_ALLOWED_BOUNDARY>[number]

const rgb = (r: U8, g: U8, b: U8) => { }

rgb(0, 23, 255) // ok
rgb(256, 23, 255) // expected error, 256 is highlighted

```
## Repetitive patterns

Sometimes we need a type which represents some repetitive patterns. For instance we have this string `"1,2; 23,67; 78,9;"`. You probably have noticed that there is a pattern `${number}, ${number};`. But how can we represent it in a TypeScript type system? There are two options. We either create a dummy function only for inference and validation purposes  or standalone type.
Let's start with a dummy function. Why am I saying that function is dummy ? Because the only purpose of this function is to make static validation of our argument. This function does nothing at runtime, it just exists.
```typescript
type Pattern = `${number}, ${number};`

type IsValid<Str extends string, Original = Str> =

    Str extends `${number},${number};${infer Rest}`
    ? IsValid<Rest, Original>
    : Str extends '' ? Original : never

const pattern = <Str extends string>(str: IsValid<Str>) => str

pattern('2,2;1,1;') // ok
pattern('2,2;1,1;;') // expected error, double semicolon ath the end

pattern('2,2;1,1;0,0') // expected error, no semicolon ath the end
```
While this function works, it has its own drawbacks. Every time we need a data structure with a repetitive pattern we should use an empty function just for the sake of static validation. Sometimes it is handy, but not everybody likes it.

However, we can do better. We can create a union with allowed variations of states.
Consider this example:
```typescript
type Coordinates = `${number},${number};`;

type Result =
    | `${number},${number};`
    | `${number},${number};${number},${number};`
    | `${number},${number};${number},${number};${number},${number};`
    | ...
```
In order to do this, we should slightly modify `ComputeRange` utility type. 
```typescript
type Repeat<
    N extends number,
    Result extends Array<unknown> = [Coordinates],
    > =
    (Result['length'] extends N
        ? Result
        : Repeat<N, [...Result, ConcatPrevious<Result>]>
    )
```
As you might have noticed, I have added `ConcatPrevious` and did not provide implementation of this type by purpose. Just want to make this mess more readable. So, in fact, we are using the same algorithm with extra `callback` - `ConcatPrevious`. How do you think we should implement `ConcatPrevious` ? It should receive the current list and return the last element + new element. Something like this:
```typescript
const ConcatPrevious = (list: string[]) => `${list[list.length-1]}${elem}`
```
Nothing complicated right? Let's do it in type scope.
```typescript
type Coordinates = `${number},${number};`;

/**
 * Infer (return) last element in the list
 */
type Last<T extends string[]> =
    T extends [...infer _, infer Last]
    ? Last
    : never;

/**
 * Merge last element of the list with Coordinates
 */
type ConcatPrevious<T extends any[]> =
    Last<T> extends string
    ? `${Last<T>}${Coordinates}`
    : never
```
Now, when we have our utility types, we can write whole type:
```typescript
type MAXIMUM_ALLOWED_BOUNDARY = 10

type Coordinates = `${number},${number};`;

type Last<T extends string[]> =
    T extends [...infer _, infer Last]
    ? Last
    : never;

type ConcatPrevious<T extends any[]> =
    Last<T> extends string
    ? `${Last<T>}${Coordinates}`
    : never

type Repeat<
    N extends number,
    Result extends Array<unknown> = [Coordinates],
    > =
    (Result['length'] extends N
        ? Result
        : Repeat<N, [...Result, ConcatPrevious<Result>]>
    )

type MyLocation = Repeat<MAXIMUM_ALLOWED_BOUNDARY>[number]

const myLocation1: MyLocation = '02,56;67,68;' // ok
const myLocation2: MyLocation = '45,56;67,68;1,2;3,4;5,6;7,8;9,10;' // ok
const myLocation3: MyLocation = '45,56;67,68;1,2;3,4;5,6;7,8;9,10,' // expected error no semicolon at the end
```
Please be aware that `MyLocation` is not some kind of infinitely repeated pattern. It is just a union of the maximum allowed number of elements. Feel free to increase `MAXIMUM_ALLOWED_BOUNDARY` until TS will throw an error. I'd be willing to bet that it should be enough for most of the cases.