I assume you are familiar with TypeScript mapped types and type inference.

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:

```
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:

```
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.

```
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"`

.

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

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

```
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.

`ForbidPx`

might be represented through this js code:

```
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.

```
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.

```
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:

```
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.

```
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.

```
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.

```
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:

```
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.

```
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:

```
const ConcatPrevious = (list: string[]) => `${list[list.length-1]}${elem}`
```

Nothing complicated right? Let's do it in type scope.

```
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:

```
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.

## Top comments (0)