6

I am working on an io-ts validation where I would like to validate the list length (it has to be between min and max). I am wondering if there's a way to achieve this behavior since it could come quite handy at runtime for API endpoint validation.

What I have so far is

interface IMinMaxArray {
  readonly minMaxArray: unique symbol // use `unique symbol` here to ensure uniqueness across modules / packages
}

const minMaxArray = (min: number, max: number) => t.brand(
  t.array,
  (n: Array): n is t.Branded<Array, IMinMaxArray> => min < n.length && n.length < max,
  'minMaxArray'
);

The code above does not work, it requires an argument for the Array-s and t.array is also not accepted. How could I make this work in a generic way?

1 Answer 1

10

Your definition is missing the typing and the codec for the array. You could make this work with a few modifications on the interface definition and extending the branded type with a codec:

interface IMinMaxArray<T> extends Array<T> {
  readonly minMaxArray: unique symbol
}

const minMaxArray = <C extends t.Mixed>(min: number, max: number, a: C) => t.brand(
  t.array(a),
  (n: Array<C>): n is t.Branded<Array<C>, IMinMaxArray<C>> => min < n.length && n.length < max,
  'minMaxArray'
);

Now you can create a definition like

minMaxArray(3,5, t.number)

If you want the definition to be more generic and composable, you could write a generic branded type that accepts a predicate:

interface RestrictedArray<T> extends Array<T> {
  readonly restrictedArray: unique symbol
}

const restrictedArray = <C>(predicate: Refinement<C[], ArrayOfLength<C>>) => <C extends t.Mixed>(a: C) => t.brand(
  t.array(a), // a codec representing the type to be refined
  (n): n is t.Branded<C[], RestrictedArray<C>> => predicate(n), // a custom type guard using the build-in helper `Branded`
  'restrictedArray' // the name must match the readonly field in the brand
)

interface IRestrictedArrayPredicate<C extends t.Mixed> {
  (array: C[]): array is ArrayOfLength<C>
}

Now you can define your restrictions. It's probably a good idea to define min and max separately, since they can be useful on their own too:

const minArray = <C extends t.Mixed>(min: number) 
  => restrictedArray(<IRestrictedArrayPredicate<C>>((array) => array.length >= min));
const maxArray = <C extends t.Mixed>(max: number)
  => restrictedArray(<IRestrictedArrayPredicate<C>>((array) => array.length <= max));

And combining these two you can define minMaxArray:

export const minMaxArray = <C extends t.Mixed>(min: number, max: number, a: C) => t.intersection([minArray(min)(a), maxArray(max)(a)])

Hope this helps.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.