The most important code linting rule: max-params
Let me ask you: What do you think this code does?
resizeImage(imagePath, 300, 200, true, true, 1)
It resizes image... but what exactly does it do? For the most part, we cannot tell without looking up the function definition.Let's say you are reviewing a PR and it includes this change:
-resizeImage(imagePath, 300, 200, true, true, 1)
+resizeImage(imagePath, 300, 200, false, true, 1)
Can you confidently say what is the impact of this change? For the most part... no. You need to know what each positional argument does.Let's say you know that the interface is:
function resizeImage(
imagePath: string,
width: number,
height: number,
upscale: boolean,
crop: boolean,
quality: number,
): Promise<Buffer>
But now a PR introduces a change to the parameter order (e.g. to make it consistent with other functions): function resizeImage(
imagePath: string,
width: number,
height: number,
+ crop: boolean,
upscale: boolean,
- crop: boolean,
quality: number,
): Promise<Buffer>
How do you review this change? Sure, reviewing the interface diff is easy, but what about the dozens or hundreds of diffs that update function invocation? -resizeImage(imagePath, 300, 200, true, false, 1)
+resizeImage(imagePath, 300, 200, false, true, 1)
resizeImage(imagePath, 300, 200, false, false, 1)
-resizeImage(imagePath, 300, 200, false, true, 1)
+resizeImage(imagePath, 300, 200, false, false, 1)
-resizeImage(imagePath, 300, 200, true, false, 1)
+resizeImage(imagePath, 300, 200, false, true, 1)
Hopefully the problem is self-explanatory: Positional arguments create a breading ground for hard and even impossible bugs to catch/debug/fix, esp. when code needs to be refactored. Fear not though as there is a better way.Let's start from the start, but this time use a single-object parameter:
resizeImage({
imagePath,
width: 300,
height: 200,
upscale: true,
crop: false,
quality: 1,
})
Can you tell what is the intention behind this code? Yes, you can get a good sense, even if you are not familiar with the implementation.Can you easily refactor the interface? Yes, linter will warn you if contract is not satisfied.
We end up with positional arguments because they feel the most natural to start with. However, as functions grow in scope, what started as a simple function with 1-2 arguments becomes an unreadable mess.
This is where [`max-params`](https://eslint.org/docs/latest/rules/max-params) comes to the rescue. Simply adding an ESLint rule that restricts having functions with more than X parameters ensures that your code remains legible and easy to refactor as your codebase scales.
3 comments
[ 3.3 ms ] story [ 19.4 ms ] threadIn this particular example, though, one might argue that picture data may be communicated in many different ways: as an URL, an FS path, a buffer, a stream...; question is: do you really want your `resizeImage()` method to read from the network or from the file system? I'd say that this is probably in violation of the principle of separation of concerns. The best choice for the first, mandatory argument is probably a stream of octets in this case, and when you can't be 100% sure, then indeed the way to design such an interface is to first constrain the method to accept a single object (dictionary) of properly named values, and at first only implement `{ stream: ..., width: ..., height: ..., ..., }`. Later on, when the software has shown it's up to the task, it will almost always be possible to introduce new parameters (say, `path` and `url`, all exclusive with `stream`), and, if need be, allow `resizeImage stream, { width: ..., ..., }` as a variant.