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
. Nothis
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…
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:
-
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()
andtest()
block.
- eg. canonical Jest test structure doesn’t require a return value at the end of every
-
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()
orserviceWorker.register()
- eg.