doup
doup

Reputation: 911

Validate a chain of mappers types

I want to validate a chain/array of mappers at type level. The idea is that for each mapper the output should match the input of the next mapper in the list. And the first input and last output are validated by the ChainedMappers<From, To> generics.

So for example:

I've tried with recursive types… but couldn't solve it. Is it doable?

interface Mapper<From, To> {
  map(from: From): To;
}

type A = /* ... */;
type B = /* ... */;
type C = /* ... */;
type D = /* ... */;

const mapperAB: Mapper<A, B> = /* ... */;
const mapperBC: Mapper<B, C> = /* ... */;
const mapperCD: Mapper<C, D> = /* ... */;

const correctMappers: ChainedMappers<A, D> = [
  mapperAB,
  mapperBC,
  mapperCD,
];

const incorrectMappers: ChainedMappers<A, D> = [
  mapperAB,
  // !!! Missing mapper from B to C !!!
  mapperCD,
];

Upvotes: 2

Views: 69

Answers (1)

Flavien Volken
Flavien Volken

Reputation: 21359

Yes this is possible, one possible way is to force the input type to at least match the expected chain. This is how it is done below despite in my case I'm using the rest operator instead of passing an array to the function. Also, my mapper can convert to and from the different types.

Note that without those TS tricks, we could have used the builder pattern.

You can test the result here

/**
 * A mapper is a simple function which convert an input of type X to an output of type Y
 */
export type Mapper<X, Y> = (x: X) => Y;

/**
 * implements this interface when you need to convert a type into another one and back
 */
export interface BiMapper<X, Y> {
  to: Mapper<X, Y>;
  from: Mapper<Y, X>;
}

export type X<T> = T extends BiMapper<infer X, any> ? X : never;

export type Y<T> = T extends BiMapper<any, infer Y> ? Y : never;

type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;


/**
 * Check is the output of the previous item matches the input of the next one
 */
export type IsValidChain<Mappers extends Array<any>> =
  Mappers extends [BiMapper<infer A, infer B>, BiMapper<infer C, infer D>, ...infer Rest]
    ? B extends C
      ? Rest[0] extends BiMapper<any, any> ?
        IsValidChain<[BiMapper<C, D>, ...Rest]>:
        true
      : false
    : Mappers extends [BiMapper<any, any>]
      ? true
      : false;

type ValidChain<Mappers extends Array<any>> = IsValidChain<Mappers> extends true ? Array<BiMapper<any, any>> : never;


type ReturnedMap<Mappers extends Array<BiMapper<any, any>>> = BiMapper<X<First<Mappers>>, Y<Last<Mappers>>>


export function chainBiMapper<T extends Array<BiMapper<any, any>>>(...mappers: T & ValidChain<T>): ReturnedMap<T>{
  return {
    to: (input) => {
      return mappers.reduce((currentValue, mapper) => mapper.to(currentValue), input);
    },
    from: (input) => {
      return mappers.reduceRight((currentValue, mapper) => mapper.from(currentValue), input);
    },
  } as  ReturnedMap<T>;
}

Upvotes: 1

Related Questions