Destructuring for type safety in JavaScript

jj

Juan Juli谩n Merelo Guerv贸s

Posted on July 25, 2023

Destructuring for type safety in JavaScript

JavaScript allows you to use classes for modularization, but it's not easy to make a function just work with a single type. Let's see an example, taken from NodEO, an evolutionary algorithm library, now in a heavy refactorization phase.

export class StringChromosome extends Chromosome {
  constructor(aString, fitness) {
    super(fitness);
    this.stringChr = aString;
  }
  // more stuff
}

export class FloatChromosome extends Chromosome {
  constructor(aVector, fitness = 0) {
    super(fitness);
    this.floatVector = aVector;
  }
  // more stuff here
}
Enter fullscreen mode Exit fullscreen mode

A chromosome has a common fitness, that is, how well it solves the problem, plus a data structure that represents the problem; EAs don't tell you which data structure you should use, so different types of chromosomes will have different data structures; we will use different names for those attributes, reflecting what they actually are.

Chromosomes undergo mutation; they essentially change randomly, looking for a better solution. A first version of a mutation operator would look like this:

// defined within FloatChromosome
static mutationRange = 0.2;
tatic mutate(chromosome) {
    const floatVector = chromosome.floatVector;
    const mutation_point = Math.floor(Math.random() * floatVector.length);
    let temp = [...floatVector];
    temp[mutation_point] =
      temp[mutation_point] -
      this.mutationRange / 2 +
      Math.random() * this.mutationRange;
    return temp;
  }
Enter fullscreen mode Exit fullscreen mode

This works as expected, returning a new data structure that can be used to build a new chromosome (chromosomes are immutable, so it will need something else to compute the fitness before we build one).

However, chromosome might or might not be a FloatVector, so we would need to add some type checks.

if (chromosome.constructor.name !== "FloatChromosome") {
      throw new Error(
        `${chromosome.constructor.name} is not a FloatChromosome`
      );
    }
Enter fullscreen mode Exit fullscreen mode

This will throw if we try to mutate a chromosome that has not been built as a FloatChromosome. So this code:

const sChrom = new StringChromosome("0001", 1);
console.log(FloatChromosome.mutate(sChrom));
Enter fullscreen mode Exit fullscreen mode

Will throw:

file:///home/jmerelo/Code/js/dev.to-js-types/lib/chromosomes.js:30
      throw new Error(
            ^

Error: StringChromosome is not a FloatChromosome
    at FloatChromosome.mutate (file:///home/jmerelo/Code/js/dev.to-js-types/lib/chromosomes.js:30:13)
    at file:///home/jmerelo/Code/js/dev.to-js-types/script/mutate.js:7:29
    at ModuleJob.run (node:internal/modules/esm/module_job:194:25)
Enter fullscreen mode Exit fullscreen mode

However, these type checks are neither complete (what if it does not have a constructor?) nor precise (what if it does actually have a floatVector attribute, even if it's not been built with that class?).

const notReallyAChrom = { floatVector: [0, 0, 0, 1] };
console.log(FloatChromosome.mutate(notReallyAChrom));
Enter fullscreen mode Exit fullscreen mode

This will still throw, although we would have been perfectly able to work with it:

file:///home/jmerelo/Code/js/dev.to-js-types/lib/chromosomes.js:30
      throw new Error(
            ^

Error: Object is not a FloatChromosome
    at FloatChromosome.mutate (file:///home/jmerelo/Code/js/dev.to-js-types/lib/chromosomes.js:30:13)
    at file:///home/jmerelo/Code/js/dev.to-js-types/script/mutate.js:7:29
    at ModuleJob.run (node:internal/modules/esm/module_job:194:25)
Enter fullscreen mode Exit fullscreen mode

Destructuring FTW

We can easily fix this using destructuring for the function arguments. Let's define the function so that it extracts from the incoming data structure just what we need, and only what we need:

static mutate({ floatVector }) {
    if (floatVector === undefined) {
      throw new Error("Incorrect data structure: no floatVector attribute");
    }
    const mutation_point = Math.floor(Math.random() * floatVector.length);
    let temp = [...floatVector];
    temp[mutation_point] =
      temp[mutation_point] -
      this.mutationRange / 2 +
      Math.random() * this.mutationRange;
    return temp;
  }
Enter fullscreen mode Exit fullscreen mode

This script:

const fChrom = new FloatChromosome([0, 0, 0, 0], 0);
console.log(FloatChromosome.mutate(fChrom));

const notReallyAChrom = { floatVector: [0, 0, 0, 1] };
console.log(FloatChromosome.mutate(notReallyAChrom));

const wrongChrom = new StringChromosome("010", 3);
console.log(FloatChromosome.mutate(wrongChrom));
Enter fullscreen mode Exit fullscreen mode

Will work correctly until it finds the last chromosome. In that case, it will produce an error. By using destructuring we find (kind of) type safety, since we will reject all invalid data structures at the same time we accept data structures we can work with, whether they are declared as a class or not.

Coda

If you really want type safety, you should go for the real type-safe JavaScript, that is, TypeScript. If you need to stay with JS for some reason (and there are many good reasons to do so), you can achieve a certain kind of cleanliness using this programming pattern.

You can find the final version of the code used in this paper in this repo. Previous versions can be found in the commit history.

馃挅 馃挭 馃檯 馃毄
jj
Juan Juli谩n Merelo Guerv贸s

Posted on July 25, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related