"Why do my colleagues dislike me?"


What is ‘eslint-plugin-fp’?

eslint-plugin-fp is an eslint plugin that enforces functional programming principles and rules on your JavaScript/TypeScript code. (eg. no mutations, no classes, no loops)

A gentle prologue

For React class components, it can be a… bloodbath 😨

Consider the following: MyComponent

Before:

Okay, so here’s what went wrong, according to eslint-plugin-fp:

  • It’s a class component. No classes
  • It’s using this. No this

Let’s remedy this situation 🔧

After:

Lint passed! 🧼

The 4 Horsemen of the (Functional Programming) Apocalypse

Since it is not a stretch to surmise that this linter would lay waste to most normal OO-centric codebases…

Here’s 4 common issues that you’d face: (coupled with their corresponding antidotes, of course 🧪)

1. no-class

Before:

type MyReactComponentState = { counter: number };

/* eslint-disable fp/no-class */
/* eslint-disable fp/no-mutation */
/* eslint-disable fp/no-this */
/* eslint-disable fp/no-let */
export class MyReactComponent extends React.Component<
  {},
  MyReactComponentState
> {
  constructor(props) {
    super(props);
    this.state = { counter: 0 };
  }

  incrementCounter() {
    this.setState((previousState) => {
      return { counter: previousState.counter + 1 };
    });
  }

  render() {
    return (
      <div>
        <span>Counter: {this.state.counter}</span>
        <button
          data-testid="button-increment"
          onClick={() => this.incrementCounter()}
        >
          Increment
        </button>
      </div>
    );
  }
}

After:

export const MyReactComponentEdit: React.FC = () => {
  const [counter, setCounter] = useState(0);

  return (
    <div>
      <span>Counter: {counter}</span>
      <button
        data-testid="button-increment"
        onClick={() => setCounter(counter + 1)}
      >
        Increment
      </button>
    </div>
  );
};
2. no-this + no-mutation

Before:

/* eslint-disable fp/no-this */
/* eslint-disable fp/no-mutation */
/* eslint-disable fp/no-class */
export class MyNameClass {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  getNamePrompt(): string {
    return `my name is ${this.name}`;
  }
}

After:

type MyNameClassEdit = { getNamePrompt: () => string };

export const makeMyNameClassEdit = (name: string): MyNameClassEdit => {
  const getNamePrompt = (): string => {
    return `my name is ${name}`;
  };

  return { getNamePrompt };
};
3. no-loops

Before:

/* eslint-disable fp/no-mutation */
/* eslint-disable fp/no-let */
/* eslint-disable fp/no-loops */
/* eslint-disable fp/no-mutating-methods */
export const capitalizeOddIndexChars = (input: string): string => {
  const splittedInput = input.split("");
  const withOddIndexCapitalized = [];

  for (let index = 0; index < splittedInput.length; index++) {
    let currentCharacter = splittedInput[index];

    const isOddIndex = index % 2 !== 0;
    if (isOddIndex) {
      currentCharacter = currentCharacter.toUpperCase();
    }

    withOddIndexCapitalized.push(currentCharacter);
  }

  const result = withOddIndexCapitalized.join("");
  return result;
};

After:

export const capitalizeOddIndexChars_edit = (input: string): string => {
  const splittedInput = input.split("");

  const withOddIndexCapitalized = splittedInput.map(
    (currentCharacter, index) => {
      const isOddIndex = index % 2 !== 0;
      if (!isOddIndex) {
        return currentCharacter;
      }
      return currentCharacter.toUpperCase();
    }
  );

  const result = withOddIndexCapitalized.join("");
  return result;
};

Instead of using the for loop, we opt for .map() instead.

4. no-throw

Before:

/* eslint-disable fp/no-throw */
export const validateItems = (items: MyItem[]): boolean => {
  items.forEach((item) => {
    const { name, price } = item;
    if (price > PRICE_THRESHOLD) {
      throw new Error(`item '${name}' price exceeded`);
    }
  });

  return true;
};

After:

type ValidateItemsEditResult = { pass: boolean, errMessage: string };

export const validateItems_edit = (
  items: MyItem[]
): ValidateItemsEditResult => {
  const initialResult: ValidateItemsEditResult = { pass: true, errMessage: "" };

  const result: ValidateItemsEditResult = items.reduce((acc, item) => {
    const { name, price } = item;
    if (price > PRICE_THRESHOLD) {
      return { pass: false, errMessage: `item '${name}' price exceeded` };
    }
    return acc;
  }, initialResult);

  return result;
};

Note: For this example, we are emulating the multi-valued return technique, which is more commonly found in Python and Golang

Example code: exp-report-eslint-plugin-fp

Recommendations

Personally, I think it boils down to whether you favor the functional programming code style. If you’re a fan of it…

I'd highly recommend using this plugin! 🤩


PS: You don’t have to drink all the functional programming kool-aid to use it (read: monads, applicative, functors). I’d wager you’d still get a lot of mileage out of this plugin, even if it enforces just a small subset of functional programming principles.

Parting Thoughts

If you do intend to use it at work, be helpful in helping your peers to write in this code style - I’d admit that it’s quite a paradigm shift, so be helpful! 🤗

Just don’t make your peers go…





Note: I am not responsible if your cultural performance rating drops because of this 😂

Until then, happy hacking! 🤓


Appendix: Dev notes

See More

For most JS/TS codebases:

  1. You’ll probably have to disable no-nil, as there’s a legitimate use case not to return a value at the end of a function.

    • eg. canonical Jest test structure doesn’t require a return value at the end of every describe() and test() block.
  2. You’ll probably have to disable no-unused-expression, as almost all React projects involving using library functions without using the function’s return value - these functions are often used to invoke side effects.

    • eg. ReactDOM.render() or serviceWorker.register()