Experience Report: eslint-plugin-fp



"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()
Please enable JavaScript to view the comments powered by Disqus.

AltStyle によって変換されたページ (->オリジナル) /