A partial archive of discourse.wicg.io as of Saturday February 24, 2024.

[Proposal] Class Constructor - class()

Randy-Buchholz
2020-07-20

Proposed:

Provide/expose a constructor for classes - class ();

Description:

The class constructor is similar to function() in that it allows creating classes from string representations.

Use Cases:

Use Case : Mixins

A Mixin is a powerful concept. The constructor class() makes mixins and inheritance by composition easy.

Composition

const frag1 = "  
   commonFunction1(){...}   
   commonFunction2(){...}  
";

const frag2 = "  
   commonFunction3(){...}  
   commonFunction4(){...}  
";

class(`Composed { ${frag1}${frag2} }`);

// Result
class Composed {
    commonFunction1(){...}
    commonFunction2(){...}
    commonFunction3(){...}
    commonFunction4(){...}
}

Mixin

class MyClass {
    function1(){...}
    function2(){...}
}

class (` NewClass { ${MyClass.toString().slice(16, -1)}, ${frag1} }`);

class NewClass {
    function1(){...}
    function2(){...}
    commonFunction1(){...}
    commonFunction2(){...}  
}

Overloads could automate this and take a name and exist class as first arguments and append class contents inside the closing }.

class( 'NewClass', MyClass, `${frag1}${frag2}` );

// Result
class NewClass {
    function1(){...}
    function2(){...}
    commonFunction1(){...}
    commonFunction2(){...}  
    commonFunction3(){...}
    commonFunction4(){...}
}

Use Case : Module Workers

The upcoming Module Worker capability supports module capabilities in Web Workers. A known class can be imported into a worker - import { MyClass } from '/MyClass.mjs';

Worker messaging supports data objects - postMessage({name : "Me"}) and more complex situations with transferables postMessage({ name: "Me", ports:ports }, [ports]). ES even has (very limited) support for the sending side of messaging a class instance. For known classes we can mimic messaging instances.

// Class Definition (TestClass.mjs)
    export class TestClass {
        name;
    }

// WorkerConstructor.mjs
    import { TestClass } from "/TestClass.mjs";
    
    globalThis.onmessage = (message) => {
        console.log(message.data);
    
        const mimic = Object.assign(new TestClass(), message.data);
        console.log(mimic);
    }


// In Parent
    import { TestClass } from "/TestClass.mjs";

    const workerPort = new Worker("/WorkerConstructor.mjs", { type: "module" });
    const instance = new TestClass();
    instance.name = "Tester";

    workerPort.postMessage(instance);

// Console Output
{name: "Tester"}
    name: "Tester"
    __proto__: Object
 
TestClass {name: "Tester"}
    name: "Tester"
    __proto__: Object


Pretty cool.

This scenario is limited to “known” classes - classes that can be imported on both sides.
Generally this means classes known at design time (using import {}), or pushed to the server in run time and dynamically imported (using import ()).

The constructor class() provides another approach. For example.

postMessage({
    type: "TestClass",
    typeDef: "TestClass { name; constructor(params){...} }",
    params: { name: "Tester" }
})

onmessage = (message) => {
    class(message.data.typeDef);
    Reflect.construct(message.data.type,[message.data.params]);
};

Many business classes/functions using specific instances would import the class definitions they use.
But framework/middleware components like message buffers and storage’s could benefit from this.
Components that operate on class “interfaces” or generic classes are supported by this.

Use Case : Generic Services and Interfaces

This is an OO, Service Oriented Architecture inspired (and very abstract) example. It is incomplete and just to explore possible use cases. Granted, it’s a somewhat non-standard approach.

Case: A Worker is deployed as a generic service that operates on class instance objects of a specific “shape” (i.e., implementing a specific interface). It calls methods specified by an “interface”.

// Interface (IShape.mjs)

export default Symbol.for("IShape");
export const testMethod = {
    name: "testMethod",
    input: {},
    returns: {}
};

// Consumer

import IShape, * as i from "/IShape.mjs";

class TestClass {
    static realizes = IShape;
    realizes() { return TestClass.realizes; }

    [i.testMethod.name]() {
        this.internalMethod();    
    }
    
    internalMethod(){ }
}

const instance = new TestClass();

worker.postMessage({
    type: TestClass.toString(),
    data: instance
});

// Provider

import IShape, * as i from "/IShape.mjs";

// in message handling

const typeHandle = class(message.data.type)
const instance = Reflect.construct(typeHandle, []);

// Not shown - hydrate instance from message data

operatedOnInterface(instance);

function operateOnInterface(iShape){
    if(iShape.realizes === IShape){
        const x = iShape.testMethod();
    }
}

The service doesn’t know or care about the specific class. No import is needed or really possible. It only needs to know the instance has some function properties.
The class() constructor allows creating an instance of the type from the class definition in the message, checking it against the imported interface specification, and calling the realized method. Because class() allows creating the type, the internal method (internalMethod) is available.