DEV Community

Alex MacArthur
Alex MacArthur

Posted on • Originally published at macarthur.me on

I didn't know you could use sibling parameters as default values in functions.

JavaScript has supported default parameter values since ES2015. You know this. I know this. What I didn't know was that you can use 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!"
Enter fullscreen mode Exit fullscreen mode

This 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.

A Scenario

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();
Enter fullscreen mode Exit fullscreen mode

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'
  );
});
Enter fullscreen mode Exit fullscreen mode

Making It More Complicated

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);
  }
Enter fullscreen mode Exit fullscreen mode

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.

Sharing Sibling Parameters

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

Nothing groundbreaking here – just a small feature that made my life a little more ergonomically pleasing. I'm hoping to come across more gems like this in the future.

SurveyJS custom survey software

JavaScript UI Libraries for Surveys and Forms

SurveyJS lets you build a JSON-based form management system that integrates with any backend, giving you full control over your data and no user limits. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay