0

Given a structure where each property is of type These<E, A> where E and A are different for each prop.

declare const someStruct: {
    a1: TH.These<E1, A1>;
    a2: TH.These<E2, A2>;
    a3: TH.These<E3, A3>;
}

I'm treating These like this

  • left: critical error, computation failed
  • right: successful computation
  • both: minor error/warning, continue computation

Now I'm looking for a way to combine results of a struct like the one above

declare function merge(struct: Record<string, TH.These<unknown, unknown>>): E.Either<CriticalErrorLeftOnly, {
    warnings: unknown[]; // these would be the E of Both
    value: Record<string, unknown>;
}>

With an Either I could do sequenceS(E.Apply)(someStruct). But this would not work here, as it would also return a left for a both.

EDIT: These<E, A> comes from fp-ts and describes a value that can be E, A or both: https://gcanti.github.io/fp-ts/modules/These.ts.html

SECOND EDIT: This is a POC of what I'm trying to achieve.. basically getting all rights and both values of a struct, while aggregating the lefts. However it is not quite there, as the result also contains the lefty properties with type never

import * as ROA from 'fp-ts/ReadonlyArray';
import * as TH from 'fp-ts/These';
import * as E from 'fp-ts/Either';

type PropertyKey = string | number | symbol;
type AnyRightThese = TH.These<any, any>;
type PropertyError<Properties> = { key: keyof Properties; error: Core.Error.Type };
type UnwrapRight<T> = T extends E.Right<infer A> ? A : never;

export const collect = <Properties extends Record<PropertyKey, AnyRightThese>>(
  properties: Properties,
): TH.These<
  PropertyError<Properties>[],
  {
    [K in keyof Properties]: UnwrapRight<Properties[K]>;
  }
> => {
  const errorsAndWarnings: PropertyError<Properties>[] = [];
  const rights: any = {};

  let isBoth = true;

  for (const k in properties) {
    const de = properties[k];

    if (TH.isLeft(de)) {
      isBoth = false;
      errorsAndWarnings.push({ key: k, error: de.left });
    } else if (TH.isRight(de)) {
      rights[k] = de.right;
    } else {
      errorsAndWarnings.push({ key: k, error: de.left });
      rights[k] = de.right;
    }
  }

  return ROA.isNonEmpty(errorsAndWarnings)
    ? isBoth
      ? TH.both(errorsAndWarnings, rights)
      : TH.left(errorsAndWarnings)
    : TH.right(rights);
};

// example
const struct = {
  a: TH.right(1),
  b: TH.left('foo'),
  c: TH.both(10, 'foobar'),
};

const a = collect(struct);

if (TH.isRight(a)) {
  a.right.b; // b should not be part of a as it is of type never
}
florian norbert bepunkt
  • 2,099
  • 1
  • 21
  • 32

1 Answers1

0

Apologies for the rather unclear formulated question. This is the solution I came up with. Probably not the most elegant, but working:

import { match } from 'ts-pattern';
import { pipe } from 'fp-ts/lib/function';
import * as E from 'fp-ts/Either';
import * as ROA from 'fp-ts/ReadonlyArray';
import * as ROR from 'fp-ts/ReadonlyRecord';
import * as S from 'fp-ts/string';
import * as TH from 'fp-ts/These';

type PropertyKey = string | number | symbol;
type AnyLeft = E.Left<any>; // eslint-disable-line
type AnyRight = E.Right<any>; // eslint-disable-line
type AnyBoth = TH.Both<any, any>; // eslint-disable-line
type AnyThese = TH.These<any, any>; // eslint-disable-line
type UnwrapLeft<T> = T extends E.Left<infer E> ? E : never;
type UnwrapRight<T> = T extends E.Right<infer A> ? A : never;

type Partitioned = {
  boths: Record<string, AnyBoth>;
  lefts: Record<string, AnyLeft>;
  rights: Record<string, AnyRight>;
};

const partition = (fa: ROR.ReadonlyRecord<string, AnyThese>): Partitioned =>
  pipe(
    fa,
    ROR.reduceWithIndex(S.Ord)({ boths: {}, lefts: {}, rights: {} } as Partitioned, (k, acc, v) =>
      match(v)
        .with({ _tag: 'Both' }, (b) => ({ ...acc, boths: { ...acc.boths, [k]: b } }))
        .with({ _tag: 'Left' }, (l) => ({ ...acc, lefts: { ...acc.lefts, [k]: l } }))
        .with({ _tag: 'Right' }, (r) => ({ ...acc, rights: { ...acc.rights, [k]: r } }))
        .exhaustive(),
    ),
  );

const collectLefts = <E>(fa: ROR.ReadonlyRecord<string, E.Left<E> | TH.Both<E, unknown>>) =>
  pipe(
    fa,
    ROR.toReadonlyArray,
    ROA.map((s) => s[1]),
    ROA.filterMap(TH.getLeft),
  );

type CollectedTheseS<Properties extends Record<PropertyKey, AnyThese>> = TH.These<
  UnwrapLeft<Properties[keyof Properties]>[],
  {
    [K in keyof Properties as Properties[K] extends TH.These<any, never> ? never : K]: UnwrapRight<
      Properties[K]
    >;
  }
>;

export const sequenceSCollectLefts = <Properties extends Record<PropertyKey, AnyThese>>(
  properties: Properties,
): CollectedTheseS<Properties> => {
  const { boths, lefts, rights } = partition(properties);
  const errors = collectLefts(lefts);
  const warnings = collectLefts(boths);

  return (
    errors.length > 0
      ? TH.left(errors)
      : warnings.length > 0
      ? TH.both(warnings, ROR.filterMap(TH.getRight)({ ...boths, ...rights }))
      : TH.right(ROR.filterMap(TH.getRight)(rights))
  ) as CollectedTheseS<Properties>;
};

florian norbert bepunkt
  • 2,099
  • 1
  • 21
  • 32