Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question (feature?)] Clean way to define a Semigroup over a (partially) partial type #1902

Open
silasdavis opened this issue Aug 11, 2023 · 1 comment

Comments

@silasdavis
Copy link
Sponsor

silasdavis commented Aug 11, 2023

Forgive my abuse of the issue types, I'm not sure if this is a feature request, perhaps there is a clean way to handle it.

I would like to define a custom Semigroup for merging structs. To preserve a sane typescript interface I'd like the input type to my merging function to be:

    type PartialOptions = {
      baseUrl: string;
      token?: string;
      extraHeaders?: Record<string, string>;
      number?: number;
    };

I can define a Semigroup that does what I want by replacing the optional fields with suitable zero values using Option for token.

Below is a solution that first normalise the input values into a type that is easier to work with in fp-ts, however it involves additional boilerplate which is a bit of a shame. Namely:

  • I cannot easily infer PartialOptions from Options (with a nod to io-ts I would like to define my 'runtime types' once and infer the compile-time type via dependent typeof types.
  • I could automate the optionify function a bit but with the primitive I know about/understand it would be more work than just defining it explicitly.

This ticket looks like it is dealing with just the same flavour of problem: #1636

Here's a test case demonstrating what I'm thinking of:

describe('fp-ts', () => {
  test('Merging an options type', () => {
    // Helper type for inferring the type implied by a Semigroup
    type SemigroupType<T> = T extends S.Semigroup<infer U> ? U : never;

    // Define a merging semigroup using Option for nullable values
    const OptionsSemigroup = S.struct({
      baseUrl: S.last<string>(),
      token: O.getMonoid(S.last<string>()),
      extraHeaders: R.getUnionSemigroup(S.last<string>()),
      // Just for fun
      number: N.SemigroupSum,
    });

    type Options = SemigroupType<typeof OptionsSemigroup>;

    const optionsMerge = S.concatAll(OptionsSemigroup);

    // We could derive this from Options, but I don't know if a generic way to handle omitted values as zero types.
    // the amount of boilerplate in the current approaches makes this clean
    type PartialOptions = {
      baseUrl: string;
      token?: string;
      extraHeaders?: Record<string, string>;
      number?: number;
    };

    function optionify({ baseUrl, token, extraHeaders = {}, number = 0 }: PartialOptions): Options {
      return {
        baseUrl,
        token: O.fromNullable(token),
        extraHeaders,
        number,
      };
    }

    // Here's what the outside world should see
    function mergeOptions(base: PartialOptions, ...options: PartialOptions[]) {
      return optionsMerge(optionify(base))(options.map(optionify));
    }

    // Here's the example usage
    const base: Options = {
      baseUrl: 'bar',
    };

    const result = mergeOptions(
      base,
      { number: 4, baseUrl: 'fff', extraHeaders: { foo: 'bar', bang: 'boo' } },
      { number: 6, baseUrl: 'fff', token: O.some('asdsadsadasd') },
      { number: 8, baseUrl: 'fff', token: 'oooooo', extraHeaders: { newKey: 'blah' } },
      { number: 10, baseUrl: 'http://example.com', extraHeaders: { foo: 'barbarbar' } },
    );
    console.log(result);
    expect(result).toStrictEqual({
      baseUrl: 'http://example.com',
      token: O.some('oooooo'),
      extraHeaders: { foo: 'barbarbar', bang: 'boo', newKey: 'blah' },
      number: 28,
    });
  });
});

If this is a poor fit for the issues section here then my apologies and I will close it.

@silasdavis silasdavis changed the title [Question (feature?)] Clean way to define a Semigroup over a partially partial type [Question (feature?)] Clean way to define a Semigroup over a (partially) partial type Aug 11, 2023
@silasdavis
Copy link
Sponsor Author

Well reading the ticket linked above again (#1636) having tried to solve the problem myself, perhaps this is exactly a dupe of that!

Side-question: not sure why the above compiles, but I am nesting options in my usage.

Is there an existing function idiom for taking (x: undefined | A | Option<A>) => Option<A>?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant