unsplash / sum-types-io-ts

io-ts bindings for @unsplash/sum-types.
https://unsplash.github.io/sum-types-io-ts/
MIT License
2 stars 1 forks source link

`getCodecFromMappedNullaryTag`: support generic input type #16

Closed OliverJAsh closed 1 year ago

OliverJAsh commented 1 year ago

Previously getCodecFromMappedNullaryTag only allowed decoding from unknown. With this PR the input type is now generic, allowing for better composition of codecs. For example, if we want to compose the codec for Country to create a new codec for Weather, we can do Country.pipe(getCodecFromMappedNullaryTag(…)(…)(…):

import * as t from "io-ts"
import * as Sum from "@unsplash/sum-types"
import { getCodecFromMappedNullaryTag } from "@unsplash/sum-types-io-ts"
import * as O from "fp-ts/Option"
import * as E from "fp-ts/Either"

type Weather = Sum.Member<"Sun"> | Sum.Member<"Rain">
const Weather = Sum.create<Weather>()
const Country = t.union([
  t.literal("UK"),
  t.literal("Italy"),
  t.literal("Spain"),
])
type Country = t.TypeOf<typeof Country>
const WeatherFromCountry: t.Type<Weather, Country, Country> =
  getCodecFromMappedNullaryTag(Weather)(
    (x: Country) => {
      switch (x) {
        // This will error because it's not a valid country.
        case "France":
          return O.some("Sun")
        case "Italy":
          return O.some("Sun")
        case "UK":
          return O.some("Rain")
        case 'Spain':
          return O.none
      }
    },
    (x): Country => (x === "Sun" ? "Italy" : "UK"),
  )(["Sun", "Rain"])
const WeatherFromCountryFromUnknown: t.Type<Weather, Country, unknown> =
  Country.pipe(WeatherFromCountry)

assert.deepStrictEqual(
  WeatherFromCountry.decode("UK"),
  E.right(Weather.mk.Rain),
)

For reference, this is the motivating real world use case from unsplash-web:

import { unpack } from 'shared/facades/Newtype';
import * as O from 'shared/facades/Option';
import * as Sum from 'shared/facades/Sum';
import * as NumericUserId from 'types/NumericUserId';

export type MockAuthUser = Sum.Member<'Subscribed'> | Sum.Member<'Any'>;
const MockAuthUser = Sum.create<MockAuthUser>();
export const { mk, match, matchX } = MockAuthUser;

export const CodecFromNumericUserId = Sum.getCodecFromMappedNullaryTag(
  MockAuthUser,
)(
  (from: NumericUserId.NumericUserId) => {
    switch (unpack(from)) {
      case 1:
        return O.some('Subscribed');
      case 3:
        return O.some('Any');
      default:
        return O.none;
    }
  },
  (to): NumericUserId.NumericUserId => {
    switch (to) {
      case 'Subscribed':
        return NumericUserId.fromNumber(1);
      case 'Any':
        return NumericUserId.fromNumber(3);
    }
  },
)(['Subscribed', 'Any'], 'MockAuthUserFromNumericUserId');
OliverJAsh commented 1 year ago

thereby allowing us to drop the default case

Potentially, but not always. See the real example here (also in the OP) which does still need the default case: https://github.com/unsplash/unsplash-web/pull/10389/files#diff-975bd6118660e97d61a5671631b8c0d1ba7a88aa181bc2e46dd502a9d2d74f01R33