What other options exist for searching objects of functions?

Can you suggest some good, easy-to-read, scalable, and efficient alternatives for this basic pattern?

type Figure =
    { kind: "square", sideLength: number } |
    { kind: "rectangle", length: number, width: number } |
    { kind: "circle", radius: number }
let calculateArea = {
    "square": function (s) { return s.sideLength * s.sideLength; },
    "rectangle": function (s) { return s.length * s.width; },
    "circle": function (s) { return s.radius * s.radius * Math.PI; },
    compute(s: Figure) {
        return calculateArea[s.kind](s);
    }
}
calculateArea.compute({ kind: "square", sideLength: 2 });

Update:

It looks like some people feel that using shapes leads to a more object-oriented approach to this issue. In my opinion, resorting to object-oriented programming for a simple function choice is unnecessary. Here's another example:

var printEntity = function (s) {
    return {
        "named": function (s) { return "&" + s.id + ";"; },
        "decimal": function (s) { return "&#" + s.id + ";"; },
        "hex": function (s) { return "&#x" + s.id + ";"; }
    }[s.kind](s);
};
printEntity({ kind: "named", id: "ndash" });

Answer №1

An easy solution would be either a switch statement or an if-else chain.

Benefit: TypeScript exhaustive checks are supported.

Surprising: This approach is just as fast as the map pattern. Tested on using Chrome 62.

type Shape =
    { kind: "square", a: number } |
    { kind: "rect", a: number, b: number } |
    { kind: "circle", r: number }

function calculateArea(shape: Shape): number {
    switch (shape.kind) {
        case "square": return shape.a * shape.a;
        case "rect": return shape.a * shape.b;
        case "circle": return shape.r * shape.r * Math.PI;
    }
    // triggers an error if all cases are not handled above
    const _exhaustiveCheck: never = shape;
    return 0;
}
calculateArea({ kind: "square", a: 2 });

Answer №2

One method to achieve this is by utilizing an object-oriented approach with prototype linkage. However, for the sake of clarity, I will be sticking to class syntax.

While this approach differs significantly in technical terms, it still offers a similar interface.

To start off, we need a base object that holds essential data and defines the interface for all shapes. It's necessary that all shapes can provide their area based on their internal data:

class Shape {

    /**
     * @param {object} data
     */
    constructor(data) {
        this.data = data;
    }

    /**
     * Computes the area of the shape. This method should be implemented by
     * all extending classes.
     *
     * @abstract
     * @return {number|NaN}
     */
    calculateArea() { return NaN; }
}

We can now establish some subclasses, where each 'implements' (or technically, 'overrides') the calculateArea method:

class Square extends Shape {
    calculateArea() {
        return Math.pow(this.data.a, 2);
    }
}

class Rect extends Shape {
    calculateArea() {
        return this.data.a * this.data.b;
    }
}

class Circle extends Shape {
    calculateArea() {
        return Math.pow(this.data.r, 2) * Math.PI;
    }
}

With these subclasses in place, we can now create new Shape-extending objects like so:

const square = new Square({ a: 1 });
const rect = new Rect({ a: 2, b: 3 });
const circle = new Circle({ r: 4 });

Nevertheless, we still need to specify the type of shape we wish to create. To enable the ability to simply include an additional property type in the given data and merge this feature with the object-oriented style, we require a builder factory. For organization purposes, let's designate that factory as a static method of Shape:

class Shape {

    /**
     * Generates a new Shape extending class if a valid type is provided,
     * otherwise generates and returns a new Shape base object.
     *
     * @param {object} data
     * @param {string} data.type
     * @return {Shape}
     */
    static create(data) {

        // It's not essential to overcomplicate this part,
        // a switch-statement suffices. Optionally, you could
        // move this to a separate factory function too.
        switch (data.type) {
            case 'square':
                return new Square(data);
            case 'rect':
                return new Rect(data);
            case 'circle':
                return new Circle(data);
            default:
                return new this(data);
        }
    }
    // ...
}

We now possess a consistent method to create shapes:

const square = Shape.create({ type: 'square', a: 1 });
const rect = Shape.create({ type: 'rect', a: 2, b: 3 });
const circle = Shape.create({ type: 'circle', r: 4 });

The final step is to have a straightforward means of directly calculating an area. This should not be too difficult:

// You can choose to implement this as a global function or as another static
// method of Shape. Whatever integrates better with your codebase:

function calculateArea(data) {
    const shape = Shape.create(data);
    return shape.calculateArea();
}

// Now you can easily calculate area:
calculateArea({ type: 'square', a: 4 }); // => 16
calculateArea({ type: 'rect', a: 2, b: 3 }); // => 6
calculateArea({ type: 'circle', r: 1 }); // 3.14159265359...

Answer №3

The example provided is almost perfect, with only a couple of minor issues to address:

  • As mentioned by Bergi in the comments, the calc function exists in the same scope as all other area kinds within the area object, thus making it impossible to have an area kind named calc.

  • The compiler does not verify that there are area calculation methods for every possible shape kind with the correct signatures.

To enhance the code slightly, consider the following adjustments:

// Although not strictly required, defining this type ensures that each member conforms to the interface with the correct 'kind' member
type WithKind<K extends string> = {[kind in K]: { kind: kind }} 

interface ShapeTypes extends WithKind<keyof ShapeTypes> {
    square: { kind: "square", a: number }
    rect: { kind: "rect", a: number, b: number }
    circle: {kind: "circle", r: number}
}

type Shape = ShapeTypes[keyof ShapeTypes];

let areaMethods: {[K in keyof ShapeTypes]: (s: ShapeTypes[K]) => number} = {
    square: s => s.a * s.a,
    rect: s =>  s.a * s.b, 
    circle: s => s.r * s.r * Math.PI,
};

function calcShapeArea(s: Shape): number {
    // Due to TypeScript limitations, the function call has to be split into two lines instead of one
    // return areaMethods[s.kind](s)
    // as a union of compatible function types is not directly callable
    const areaMethod: (s: Shape) => number = areaMethods[s.kind];
    return areaMethod(s);
}

Answer №4

When ECMAScript 2015 was released, it brought along the introduction of the Map object.

map = new Map([
    [9, "nine"],
    [0, "zero"],
    [2, "two"],
    ['a', "a"],
    [{}, "empty object"],
    [x => x*x, "function"],
    [1, "one"],
]);

for (const [key, value] of map) {
    console.log(`${key} -> ${value}`);
}

One practical example of using a Map in JavaScript could be the following:

function area(s) {
    return new Map([
        ["square", s => s.a * s.a],
        ["rect", s => s.a * s.b],
        ["circle", s => s.r * s.r * Math.PI]
    ]).get(s.kind)(s);
}
area({ kind: "square", a: 2 });

It's important to note that as of 2017, this feature is not supported by all web browsers and also not supported by TypeScript.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Steps for deploying an ejs website with winscp

I have developed a Node.js web application using ExpressJS, with the main file being app.js. Now I need to publish this website on a domain using WinSCP. However, WinSCP requires an index.html file as the starting point for publishing the site. Is there ...

What techniques does Google use to craft mobile-friendly fixed backgrounds and parallax content within iframes?

Currently, I am working on a test that involves utilizing an intersectionobserver in conjunction with iframe postMessage to adjust the background image in a translate3d manner within the iframe. However, this method is causing significant jitter and potent ...

Change the content of a selectbox dynamically with the help of jQuery and AJAX

Currently, I am exploring the best approach for a specific challenge: I have various categories, subcategories, sub-subcategories, and so on, that I need to display in separate select boxes. For instance, initially, the options may look like this: <sel ...

Ways of retrieving Sveltekit session data in an endpoint

Is there a way to access a session in an endpoint using SvelteKit? I attempted the following with no success: import { get } from 'svelte/store'; import { getStores} from "$app/stores"; function getUser() { // <- execute this du ...

Accessing different pages in Angular 2 based on user type

If I have four pages and two user types, how can we implement access control in Angular 2 so that one user can access all four pages while the other is restricted to only two pages? ...

Issue with Webpack failing to bundle a custom JavaScript file

Here is the structure of my directory: Root -dist -node_modules -src --assets --css --js --scss --index.js --template.html --vendor.js package-lock.json package.json postcss.config.js tailwind.config.js common.config.js development.config.js production.co ...

What is the method for modifying the array that has been generated using Vue's "prop" feature?

According to the Vue documentation, a prop is passed in as a raw value that may need transformation. The recommended approach is to define a computed property using the prop's value. If the "prop" is an array of objects, how can it be transformed int ...

What is the simplest way to test an npm module while coding?

Currently, I am in the process of making modifications to @editorjs/nested-list. To streamline my testing process without extensive installations, I have created a simple web page: <html> <head> <script src="https://cdn.jsdelivr.net/npm ...

Using JQuery, identify cells located in the first column of a table excluding those in the header section

In the past, I had code that looked like this: $(elem).parents('li').find(...) I used this code when elem was an item in a list, making it easy to reference all items in the list. But now, I've made some changes and decided to use a table ...

Monitor modifications to documents and their respective sub-collections in Firebase Cloud Functions

Is it possible to run a function when there is a change in either a document within the parent collection or a document within one of its subcollections? I have tried using the code provided in the Firebase documentation, but it only triggers when a docume ...

What causes the `d-none` class to toggle on and off repeatedly when the distance between two div elements is less than 10 during window resizing?

My primary objective is to display or hide the icon based on the width of the right container and the rightButtonGroupWrapper. If the difference between these widths falls below 10, the icons should be shown, otherwise hidden. However, I'm facing an i ...

Retrieving a list of numbers separated by commas from an array

Currently, I'm retrieving data from a MYSQL database by executing the following SQL command: SELECT GROUP_CONCAT(MemberMemberId SEPARATOR ',') AS MemberMemberId FROM member_events WHERE event_date = "2000-01-01" AND Eve ...

Developers specializing in Google Maps navigate to a particular destination

I have been working on an app that provides users with the best options for places to visit, utilizing Google Maps technology. Here is what I have accomplished so far: Show the user their current location Show the user possible destinations (with marker ...

Issue on Heroku with Discord.js displaying a "Service Unavailable" message

Encountered a strange error while trying to launch my discord bot on heroku. Previously, I implemented a command handler for the bot to organize commands in separate files but faced multiple code errors. With the help of a member from this community, all e ...

Replacing multiple attributes within a complex HTML opening tag using Node.js regular expressions

I'm currently working on a Node.js project where we need to search through several PHP view files and replace certain attributes. My goal is to extract the values of HTML open tag attributes and replace them. To clarify, I want to capture anything in ...

Creating vibrant row displays in Vue.js: A guide to styling each row uniquely

I am not a front-end developer, so Vue and JS are new concepts for me. Currently, I am working on an application for managing sales. One of the components in my project involves displaying invoices in a list format. To make the rows visually appealing, I ...

Initiate a series of tasks and await their completion using RxJS / Redux Observables

My app requires an initialisation action to be fired, followed by a series of other actions before triggering another action. I found a similar question on Stack Overflow However, when attempting this approach, only the initial APP_INIT action is executed ...

Can you create a while loop that continuously asks for user input, stores the responses, and then makes them accessible in an array?

When developing a Yeoman generator, the necessary dependencies can be found at https://github.com/sboudrias/mem-fs-editor#copytplfrom-to-context-settings and https://github.com/SBoudrias/Inquirer.js/. The main goal is to prompt the user with a question an ...

Unable to process JSON array

One issue I'm facing is with an 'onload' handler for my web page. The javascript function 'handleLoad()' that it calls has suddenly stopped working, specifically not being invoked after attempting to pass the output of json_encode ...

Enhancing CKEditor: Inserting new elements upon each dialog opening

I am facing a challenge where I need to dynamically add varying numbers of elements to a dialog window each time it is opened. Below is the code I am working with: CKEDITOR.on( 'dialogDefinition', function(ev) { var dialogName = ev.data.name ...