function refineSwitch
refineSwitch<
TBase,
TRefiners extends Record<string, Refiner<TBase, any, []>>
>
(
baseCoder: Coder<TBase>,
refiners: TRefiners,
selector: { refine: (
base: TBase,
context: Context
) => keyof TRefiners | null
; unrefine: (
context: Context
) => keyof TRefiners | null
; }
): Coder<RefinedUnion<TRefiners>>

Creates a coder that conditionally applies refiners based on selector functions, using switch-like semantics for bidirectional encoding and decoding.

This primitive enables multi-stage conditional coding where different refiners are applied based on runtime values. The selector functions determine which refiner to use for both decode (refine) and encode (unrefine) operations.

Switch Statement Analogy:

// Traditional switch
switch (getKey(value)) {
  case 'A': return refinerA(value);
  case 'B': return refinerB(value);
  default: throw new Error('No match');
}

// refineSwitch equivalent
refineSwitch(baseCoder, refiners, {
  refine: (value) => getKey(value),    // switch expression for decode
  unrefine: (value) => getKey(value),  // switch expression for encode
})

Discriminator Stability Contract: The selector must return the same key for a value before and after refinement. Violating this contract will cause encoding to fail or produce incorrect results.

Example violation:

refiner.refine({ type: "A", ... }) → { type: "B", ... } // INVALID - type changed

Error Handling: If the selector returns null, an error is thrown. This fail-fast behavior ensures type safety and prevents unexpected values from being processed.

Examples

PNG chunk refinement with type-based selection

import { assertEquals } from "@std/assert";
import { refineSwitch, type Refiner, type Context, decode, encode } from "@hertzg/binstruct";
import { struct, u32be, bytes, string, ref } from "@hertzg/binstruct";

interface PngChunkUnknown {
  length: number;
  type: Uint8Array;
  data: Uint8Array;
  crc: number;
}

interface IhdrChunk {
  length: number;
  type: "IHDR";
  data: { width: number; height: number };
  crc: number;
}

const lengthCoder = u32be();
const pngChunkUnknown = struct({
  length: lengthCoder,
  type: bytes(4),
  data: bytes(ref(lengthCoder)),
  crc: u32be(),
});

const ihdrRefiner = (): Refiner<PngChunkUnknown, IhdrChunk, []> => {
  const typeCoder = string(4);
  const dataCoder = struct({ width: u32be(), height: u32be() });

  return {
    refine: (chunk, ctx) => ({
      ...chunk,
      type: decode(typeCoder, chunk.type, ctx) as "IHDR",
      data: decode(dataCoder, chunk.data, ctx),
    }),
    unrefine: (chunk, ctx) => ({
      ...chunk,
      type: encode(typeCoder, chunk.type, ctx, new Uint8Array(4)),
      data: encode(dataCoder, chunk.data, ctx, new Uint8Array(8)),
    }),
  };
};

const pngChunkCoder = refineSwitch(
  pngChunkUnknown,
  { IHDR: ihdrRefiner() },
  {
    refine: (chunk: PngChunkUnknown, ctx: Context) => {
      const type = decode(string(4), chunk.type, ctx);
      return type === "IHDR" ? "IHDR" : null;
    },
    unrefine: (chunk: IhdrChunk, _ctx: Context) => chunk.type === "IHDR" ? "IHDR" : null,
  }
);

const buffer = new Uint8Array(100);
const chunk: IhdrChunk = {
  length: 8,
  type: "IHDR",
  data: { width: 100, height: 200 },
  crc: 0,
};

const written = pngChunkCoder.encode(chunk, buffer);
const [decoded] = pngChunkCoder.decode(buffer);

assertEquals((decoded as IhdrChunk).type, "IHDR");
assertEquals((decoded as IhdrChunk).data.width, 100);

Supporting unknown values with explicit fallback refiner

import { assertEquals } from "@std/assert";
import { refineSwitch, type Refiner, type Context } from "@hertzg/binstruct";
import { struct, u8, bytes } from "@hertzg/binstruct";

interface BaseMsg {
  type: number;
  data: Uint8Array;
}

interface KnownMsg {
  type: 1;
  value: number;
}

interface UnknownMsg {
  type: number;
  data: Uint8Array;
}

const knownRefiner = (): Refiner<BaseMsg, KnownMsg, []> => ({
  refine: (msg, _ctx) => ({ type: 1, value: msg.data[0] }),
  unrefine: (msg, _ctx) => ({ type: 1, data: new Uint8Array([msg.value]) }),
});

const unknownRefiner = (): Refiner<BaseMsg, UnknownMsg, []> => ({
  refine: (msg, _ctx) => msg,
  unrefine: (msg, _ctx) => msg,
});

const baseCoder = struct({ type: u8(), data: bytes(1) });

const msgCoder = refineSwitch(
  baseCoder,
  {
    known: knownRefiner(),
    unknown: unknownRefiner(),
  },
  {
    refine: (msg: BaseMsg, _ctx: Context) => msg.type === 1 ? "known" : "unknown",
    unrefine: (msg: KnownMsg | UnknownMsg, _ctx: Context) => ("value" in msg) ? "known" : "unknown",
  }
);

const buffer = new Uint8Array(10);
const unknown: UnknownMsg = { type: 99, data: new Uint8Array([42]) };

// @ts-ignore - Complex union type inference
const written = msgCoder.encode(unknown, buffer);
const [decoded] = msgCoder.decode(buffer);

assertEquals((decoded as UnknownMsg).type, 99);
assertEquals((decoded as UnknownMsg).data[0], 42);

Type Parameters

TBase

The base type decoded by the base coder

TRefiners extends Record<string, Refiner<TBase, any, []>>

Record mapping selector keys to refiners

Parameters

baseCoder: Coder<TBase>

The coder that decodes the base value

refiners: TRefiners

Record mapping keys to refiners that transform base values

selector: { refine: (
base: TBase,
context: Context
) => keyof TRefiners | null
; unrefine: (
context: Context
) => keyof TRefiners | null
; }

Functions that select which refiner to use based on value

Return Type

A coder that produces a union of all refined types