Search code examples
javascripttypescriptoopconstructorfunctional-programming

TypeScript functional programming patterns for comfortable object construction?


I'm having a hard time finding examples (videos or blogs) of functional programming object construction patterns.

I recently encountered the below snipped builder pattern and I like the experience it provides for constructing an object with nesting. When it's a flatter object, I'd normally just use a simple object factory with an opts param to spread with defaults, but passing in a nested array of objects starts to feel messy.

Are there FP patterns that can help make composing an object with nesting like the below comfortable while allowing for calling some methods n times, such as addStringOption?

const data = new SlashCommandBuilder()
    .setName('echo')
    .setDescription('Replies with your input!')
    .addStringOption(option =>
        option.setName('input')
            .setDescription('The input to echo back')
            .setRequired(true)
    )
    .addStringOption(option =>
        option.setName('category')
            .setDescription('The gif category')
            .setRequired(true)
            .addChoices(
                { name: 'Funny', value: 'gif_funny' },
                { name: 'Meme', value: 'gif_meme' },
                { name: 'Movie', value: 'gif_movie' },
  ));

data ends up looking something like:

{
  name: "echo",
  description: "Replies with your input!",
  options: [
    {
      name: "input",
      description: "The input to echo back",
      type: 7, // string option id
      required: true,
      choices: null,
    },
    {
      name: "category",
      description: "The gif category",
      required: true,
      type: 7,
      choices: [
        { name: "Funny", value: "gif_funny" },
        { name: "Meme", value: "gif_meme" },
        { name: "Movie", value: "gif_movie" },
      ],
    },
  ],
};

Below is what I'm playing around with. I'm still working on learning how to type them in TS so I'm sharing the JS.

Allowing for method chaining in the below snippet is maybe contorting FP too much make it like OOP, but I haven't found an alternative that makes construction flow nicely.

An alternative could be standalone builders each returning a callback that returns the updated state then pipe these builders together, but with some builders being callable n times it's hard to make and provide the pipe ahead of time and without the dot notation providing intellisense it seems harder to know what the available functions are to build with.

const buildCommand = () => {
  // data separate from methods.
  let command = {
    permissions: ['admin'],
    foo: 'bar',
    options: [],
  };

  const builders = {
    setGeneralCommandInfo: ({ name, description }) => {
      command = { ...command, name, description };
      // trying to avoid `this`
      return builders;
    },

    setCommandPermissions: (...permissions) => {
      command = { ...command, permissions };
      return builders;
    },

    addStringOption: (callback) => {
      const stringOption = callback(buildStringOption());
      command = { ...command, options: [...command.options, stringOption] };
      return builders;
    },
    // can validate here
    build: () => command,
  };

  return builders;
};

const buildStringOption = () => {
  let stringOption = {
    choices: null,
    type: 7,
  };

  const builders = {
    setName: (name) => {
      stringOption = { ...stringOption, name };
      return builders;
    },

    setDescription: (description) => {
      stringOption = { ...stringOption, description };
      return builders;
    },

    addChoices: (choices) => {
      stringOption = { ...stringOption, choices };
      return builders;
    },

    build: () => stringOption,
  };

  return builders;
};

const command1 = buildCommand()
  .setGeneralCommandInfo({ name: 'n1', description: 'd1' })
  .setCommandPermissions('auditor', 'moderator')
  .addStringOption((option) =>
    option.setName('foo').setDescription('bar').build()
  )
  .addStringOption((option) =>
    option
      .setName('baz')
      .setDescription('bax')
      .addChoices([
        { name: 'Funny', value: 'gif_funny' },
        { name: 'Meme', value: 'gif_meme' },
      ])
      .build()
  )
  .build();

console.log(command1);

Solution

  • Why not simply create and use data constructors?

    const SlashCommand = (name, description, options) =>
      ({ name, description, options });
    
    const StringOption = (name, description, required, type = 7, choices = null) =>
      ({ name, description, required, type, choices });
    
    const Choice = (name, value) => ({ name, value });
    
    const data = SlashCommand('echo', 'Replies with your input!', [
      StringOption('input', 'The input to echo back', true),
      StringOption('category', 'The gif category', true, undefined, [
        Choice('Funny', 'gif_funny'),
        Choice('Meme', 'gif_meme'),
        Choice('Movie', 'gif_movie')
      ])
    ]);
    
    console.log(data);

    TypeScript Playground Example

    You can't get more functional than this. Intellisense will also help you with the constructor arguments.