Colum
function Image(width, height = width)
JavaScript has supported default parameter values since ES2015. You know this. I know this. What I didn't know was that you can use previous sibling parameters as the default values themselves. (Or maybe "adjacent positional parameters"? Not sure what to call these.)
function myFunc(arg1, arg2 = arg1) {
console.log(arg1, arg2);
}
myFunc("arg1!");
// "arg1!" "arg1!"
MDN even calls it out (I didn't find out until after publishing this post), demonstrating how the feature could help with some unusual function signatures.
It works in class constructors too – something I found to be quite helpful in making some PicPerf.io code more testable. It's common to see simple dependency injection used to that end. Let's explore it a bit.
Keeping with the image optimization theme, say you have an OptimizedImage
class. Provide an image URL to its constructor, and you can retrieve either a freshly optimized buffer of the image or a cached version.
class OptimizedImage {
constructor(
imageUrl: string,
cacheService = new CacheService(),
optimizeService = new OptimizeService()
) {
this.imageUrl = imageUrl;
this.cacheService = cacheService;
this.optimizeService = optimizeService;
}
async get() {
const cached = this.cacheService.get(this.imageUrl);
// Return the previously optimized image.
if (cached) return cached;
const optimizedImage = await this.optimizeService
.optimize(this.imageUrl);
// Cache the optimized image for next time.
return this.cacheService.put(this.imageUrl, optimizedImage);
}
}
const instance = new OptimizedImage('https://macarthur.me/me.jpg');
const imgBuffer = await instance.get();
The only constructor parameter used in production is imageUrl
, but injecting CacheService
and OptimizeService
enables easier unit test with mocks:
import { it, expect, vi } from 'vitest';
import { OptimizedImage } from './main';
it('returns freshly optimized image', async function () {
const fakeImageBuffer = new ArrayBuffer('image!');
const mockCacheService = {
get: (url) => null,
put: vi.fn().mockResolvedValue(fakeImageBuffer),
};
const mockOptimizeService = {
optimize: (url) => fakeImageBuffer,
};
const optimizedImage = new OptimizedImage(
'https://test.jpg',
mockCacheService,
mockOptimizeService
);
const result = await optimizedImage.get();
expect(result).toEqual(fakeImageBuffer);
expect(mockCacheService.put).toHaveBeenCalledWith(
'https://test.jpg',
'optimized image'
);
});
In that example, both of those service classes use imageUrl
only when particular methods are invoked. But imagine if they required it to be passed into their own constructors instead. You might be tempted to pull instantiation into OptimizedImage
's constructor (I was):
class OptimizedImage {
constructor(
imageUrl: string
) {
this.imageUrl = imageUrl;
this.cacheService = new CacheService(imageUrl);
this.optimizeService = new OptimizeService(imageUrl);
}
That’d work, but now OptimizedImage
is fully responsible for service instantiation, and testing becomes more of a hassle too. It's not so easy to pass in mocks for service instances.
You could get around this by passing in mock class definitions, but you'd then need create mock versions of those classes with their own constructors, making testing more tedious. Fortunately, there's another option: use the imageUrl
parameter in the rest of your argument list.
I wasn't aware this was even possible until a little while ago. Here's how it'd look:
export class OptimizedImage {
constructor(
imageUrl: string,
// Use the same `imageUrl` in both dependencies.
cacheService = new CacheService(imageUrl),
optimizeService = new OptimizeService(imageUrl)
) {
this.cacheService = cacheService;
this.optimizeService = optimizeService;
}
async get() {
const cached = this.cacheService.get();
// Return the previously optimized image.
if (cached) return cached;
const optimizedImage = await this.optimizeService.optimize();
// Cache the optimized image for next time.
return this.cacheService.put(optimizedImage);
}
}
With this setup, you're able to mock those instances just as easily as before, and the rest of the class doesn't even need to hold onto an instance of imageUrl
itself. Instantiation, of course, still remains simple:
const instance = new OptimizedImage('https://macarthur.me/me.jpg');
const img = await instance.get();
The same testing approach remains in tact as well:
import { it, expect, vi } from 'vitest';
import { OptimizedImage } from './main';
it('returns freshly optimized image', async function () {
const mockCacheService = {
get: () => null,
put: vi.fn().mockResolvedValue('optimized image'),
};
const mockOptimizeService = {
optimize: () => 'optimized image',
};
const optimizedImage = new OptimizedImage(
'https://test.jpg',
mockCacheService,
mockOptimizeService
);
const result = await optimizedImage.get();
expect(result).toEqual('optimized image');
expect(mockCacheService.put).toHaveBeenCalledWith('optimized image');
});
Nothing revolutionary here – just a feature that makes a particular problem a little more ergonomic to solve.
Beyond something like this, I haven't come across a use case when this feature is especially handy. But there may be some opportunity to harden your job security and establish your undefeated cleverness. Here's a fun one oculus42 dropped on Reddit.
Consider calculating the sum of an array of numbers with .reduce()
:
const numbers = [1, 2, 3, 4, 5];
const total = numbers.reduce((acc, value) => {
return acc + value;
});
console.log(total); // 15
The "business" for this process lives in the function body. But you could shove it into function signature as a default parameter too, leaving the body to only return the result.
const total = numbers.reduce(
(acc, value, _index, _array, result = acc + value) => {
return result;
}
);
And it can use an implicit return
to look even more impressive:
const total = numbers.reduce((acc, value, _index, _array, result = acc + value) => result);
Pretty weird. Not tremendously useful from my own experience, but interesting nonetheless. Eager to come across more gems like this in the future.
Alex MacArthur is a software engineer working for Dave Ramsey in
Nashville-ish, TN.
Soli Deo gloria.
Get irregular emails about new posts or projects.
No spam. Unsubscribe whenever.function Image(width, height = width)
Some signatures profit:
function findIn(find, str, start = 0, end = str.length) {}
Beautiful.