typescript - io-ts loggar dekodningsfel för unionstyper i arrayer


Jag använder io-ts och försöker avkoda inbäddade arrayer av värden. Standardbeteendet i io-ts är att om något objekt i en array misslyckas, misslyckas hela arrayen. Jag ville fortfarande att arrayen skulle passera och bara returnera de giltiga objekten samt kunna logga de ogiltiga. Jag skrev en egen codec som utökar arrayen med withValidate från io-ts-types.

export function validItemsOnlyArray<C extends t.Mixed>(theType: C) {
  return withValidate(t.array(theType), (itemToDecode, context) => {
    const pipeResult = pipe(
      // validate that the value is at least an array
      // if it passes, then chain and validate each item
      t.UnknownArray.validate(itemToDecode, context),
      FpTsEither.chain((validArrayObject) => {
        const decoded = pipe(
          validArrayObject,
          FpTsArray.map((arrayItem) => {
            const decodeResult = theType.decode(arrayItem)
            if (FpTsEither.isLeft(decodeResult))
              console.log(
                `${getPaths(decodeResult)} unable able to decode ${
                  theType.name
                }`,
              )
            //TODO: figure out how to log errors, currently logging for each failed union type
            return isRight(decodeResult)
              ? t.success(decodeResult.right)
              : t.failure(arrayItem, context)
          }),
          FpTsArray.separate,
        )

        return t.success(decoded.right)
      }),
    )
    return pipeResult
  })
}

Avkodningen fungerar perfekt. Däremot får jag falska positiva i mina loggningsmeddelanden eftersom jag använder unionstyper. Om en av unionerna misslyckas försöker den nästa tills den lyckas. Detta returnerar rätt resultat, men det loggar också misslyckandena när en unionstyp misslyckas med att avkoda.

Jag behöver ett sätt att veta att vi försöker avkoda en unionstyp och att det fortfarande finns andra objekt att försöka innan vi loggar att alla möjliga alternativ har försökt. Jag vet att det finns mycket information tillgänglig i context men var inte säker på om det fanns ett bättre sätt att veta och sammanfoga allt.

Här är objekten jag försöker avkoda

const Competitor = t.exact(
      t.intersection([
        t.type({
          isWinner: t.boolean,
          name: t.string,
          shortName: t.string,
        }),
        t.partial({
          gender: t.union([t.string, t.null]),
          dateOfBirth: t.union([t.string, t.null]),
          zipCode: t.union([t.string, t.null]),
          city: t.union([t.string, t.null]),
          state: t.union([t.string, t.null]),
        }),
      ]),
    )

    const SportResult = t.exact(
      t.intersection([
        t.type({
          arenaBoutGuid: t.string,
          arenaEventGuid: t.string,
          competitors: validItemsOnlyArray(Competitor),
          isDual: t.boolean,
          dateModified: t.string,
          eventName: t.string,
          sport: t.string,
          siteId: t.number,
          type: t.string,
        }),
        t.partial({
          boutNumber: t.union([t.string, t.null]),
          winType: t.union([t.string, t.null]),
        }),
      ]),
    )
    const Comp = t.intersection([
      t.type({
        name: t.string,
        id: t.string,
        isWinner: t.boolean,
        points: t.union([t.number, t.null]),
      }),
      t.partial({
        shortName: t.union([t.string, t.null]),
        coreNodeId: t.union([t.number, t.null]),
      }),
    ])

    const Sport2Result = t.exact(
      t.intersection([
        t.type({
          arenaBoutGuid: t.string,
          arenaEventGuid: t.string,
          competitors: validItemsOnlyArray(Comp),
        }),
        t.partial({
          winType: t.union([t.string, t.null]),
          matchScore: t.union([t.string, t.null]),
          weightClassId: t.union([t.string, t.null]),
          weightClassName: t.union([t.string, t.null]),
        }),
      ]),
    )

När jag avkodar här decodas resultatet korrekt som Right, men jag får meddelandet i console.log att det misslyckades.

    const Response = t.type({
      data: validItemsOnlyArray(t.union([Sport2Result, SportResult])),
    })

   const result = Response.decode({
  data: [
    {
      arenaBoutGuid: 'xyz',
      arenaEventGuid: 'abvc',
      competitors: [
        {
          isWinner: true,
          name: 'M Name',
          shortName: 'M. Name',
        },
        {
          isWinner: false,
          name: 'A Name',
          shortName: 'A. Name',
        },
      ],
      siteId: 1,
      dateModified: '2022-05-06T22:03:17.060Z',
      matchDateTime: '2022-05-06T22:02:01.417Z',
      isDual: false,
      ventName: 'Event',
      sport: 'my sport',
      matchScore: '10-0 3:03',
      type: 'match',
    },
  ],
})

All hjälp skulle uppskattas!

Svar #1

Det enda svaret jag kan komma på som uppnår det du hoppas på och inte kräver att du skriver din egen anpassade wrapper runt io-ts för att lägga till tillstånd i din avkodning, är att avstå från loggning i den anpassade codec som separerar felen.

Problemet kokar ner till det faktum att varje värde potentiellt kommer att valideras flera gånger när man använder t.union, så loggningen kan inte ske inom valideringsfunktionen och måste skjutas upp till en senare tidpunkt.

Jag tror att det enklaste tillvägagångssättet skulle vara att ändra returtypen för din anpassade codec till att returnera Separated<t.Errors[], A[]>. Detta innebär att du i din kod, när du använder det avkodade värdet, måste titta på field.right, och felinformationen kommer att följa med dig, men det verkar inte finnas något annat sätt att hantera de olika möjliga sätten att tolka left-värdena när unionfält är involverade.

Med minimala ändringar i din kod skulle detta se ut som:

// Previous imports +
import { flow } from 'fp-ts/lib/function';

interface IFailure {
  typeName: string;
  failure: t.Errors;
}

export function validItemsOnlyArray<C extends t.Mixed>(theType: C) {
  return withValidate(t.array(theType), (itemToDecode, context) => {
    const pipeResult = pipe(
      // validate that the value is at least an array
      // if it passes, then chain and validate each item
      t.UnknownArray.validate(itemToDecode, context),
      FpTsEither.map(flow(
          FpTsArray.map(theType.decode),
          FpTsArray.separate,
          // Adding in a bit of context so the error can be crafted later.
          FpTsSeparated.mapLeft((l): IFailure[] => l.map(f => ({
            failure: f,
            typeName: theType.name,
          }))),
          FpTsEither.right, // Or t.success
        )
      ),
    )
    return pipeResult
  })
}

I praktiken kommer du att behöva gå igenom de värden som parseats framgångsrikt för att söka efter objekt som inte parseats framgångsrikt av någon av grenarna i union och sedan lägga till loggning bara för dessa objekt. Den logiken kan förmodligen vara bättre att ha i en hjälpfunktion, snarare än som en del av dina codecs ändå.

Du skulle kunna förenkla arbetet med detta något med en hjälpare som:

function getOrLog<A>(s: Separated<IFailure[], A[]>): A[] {
  s.left.forEach((e) => {
    console.log(
      `${getPaths(e.failure)} unable able to decode ${e.typeName}`
    );
  });
  return s.right;
}

Detta skulle åtminstone ta dig tillbaka till där du är nu, när du vill använda ett av fälten, men du kan också bara använda fältets right-värde direkt.

I fallet med unionen, som jag nämnde, kommer du sannolikt att behöva titta igenom left-värdena i en aggregeringsfunktion under en viss tid för att hitta värden som inte lyckades någonstans.

Alternativt tillvägagångssätt

Det andra alternativet som presenterar sig är att anpassa union som du nämnde. Jag tror att detta kommer att bli en mer komplicerad uppgift, men det skulle vara möjligt att skapa en parallell abstraktion som internt använder io-ts men som lägger till state i dekoderingsprocessen. Detta skulle tillåta din anpassade codec att skriva direkt i den strömmen och du skulle kunna definiera en anpassad union-hjälpare för att hantera strömmen när dess barn använder den.

Även om detta skulle erbjuda mer flexibilitet, kommer det med en betydande kostnad eftersom man behöver bygga upp en spegel av io-ts för varje specialfall man vill stödja, vilket är anledningen till att jag i slutändan inte lade mycket tid på att överväga det.