maintaining a JS scope in a Custom Element constructor call - javascript

I am experimenting with a new pattern,
where each newly created Card element
uses the constructor scope to store (private) variables:
pseudo code:
class CardtsCard extends HTMLElement {
constructor(){
let private = 666;
Object.assign(this,{
getPrivate : () => console.log(private)
});
}
get private(){
//can not access scope in constructor
return 42;
}
}
So:
el.getPrivate(); // outputs 666
el.private; // outputs 42
I have loads of getters and setters
and sticking data ON my elements with this.whatever=value feels a bit weird.
I can extend it to:
class CardtsCard extends HTMLElement {
constructor(){
let private = new Map();
Object.assign(this,{
setPrivate : (data,value) => private.set(data,value),
getPrivate : (data) => private.get(data)
});
}
}
Question: I am no JS scope expert, are there drawbacks?

Whenever you call the constructor in the first example, a closure has to be created which contains these "private" variables. For a few elements that doesn't cause any overhead, but for a lot of elements you might see a performance and memory overhead.
Concerning your second example: Map lookups can't be inlined, property lookups can, so they are way more performant (which you will only notice on many operations).
And what benefit do you have through this setup? A public method getPrivate isn't really private isn't it? If you need private variables, mark them as private in some way, and trust everyone using the code to deal with it appropriately.

Related

How can I make TypeScript aware that `classInstance.constructor` in JS will give access to the class' static methods and members?

The following code works in plain JavaScript:
class Post {
static table = "Posts"
title = ""
}
const post = new Post()
console.log(post.constructor.table) // will log "Posts"
But for some reason, TypeScript does not seem to be aware that the constructor member of a class instance gives access to that class' static methods and members. TypeScript will insist that post.constructor is nothing more than the same function as Object.constructor.
This is problematic in a project that I'm working on. So I wonder, is there a way to make TypeScript aware that post.constructor.table does actually exist?
I have found a way that works, but it is super ugly and I really hope there is another way.
import type { Class, Constructor, EmptyObject } from "type-fest"
// Here I'm overriding the constructor function on a class instance type,
// because in plain javascript it's also possible to access
// the constructor function from a class instance and then
// get access to all the class' static methods and members.
type OverrideConstructorToIncludeStatic<
T,
Static = EmptyObject,
Arguments extends unknown[] = any[],
> = T & { constructor: Static & Constructor<T, Arguments> }
// This augmented Class type allows one to define
// expected static methods and members on classes.
type ClassWithStatic<
T,
Static = EmptyObject,
ConstructorArguments extends unknown[] = any[],
> = Class<
OverrideConstructorToIncludeStatic<T, Static, ConstructorArguments>,
ConstructorArguments
> & Static
type HasTable = { table: string }
type ClassHasTable<T> = ClassWithStatic<T, HasTable>
function extend<T>(classType: Class<T> & HasTable): ClassHasTable<T> {
return classType as ClassHasTable<T>
}
const Post = extend(class Post {
static table = "Posts"
title = ""
})
const post = new Post()
post.constructor.table // now TypeScript won't complain
So that works, but const Post = extend(class Post { ... }) is so ugly. And moreover, I'm not the one who would need to write that. The consumers of my library would need to write that and I don't want to do that to anyone.
Is there any other way of doing this?
Edit
Of course I'm using over-simplified examples here. In case you'd like to know the real use-case I'm asking this for, you can check out this playground: https://tsplay.dev/wXqeDN.
In short, I'm trying to see if I can figure out a way to (un)serialize class objects that adhere to a certain interface requiring static methods for the (un)serializing process. The end goal would be that I could serialize class objects in a backend and unserialize them in frontend code.
But I just realized that that might not be possible after all, because I would probably also need to serialize the classRegister Map you see in that playground. And I'm afraid that may prove to be impossible. Unless I'd use something like eval, but we all know how dangerous that is.
Corrected thanks to caTS's observation
Since constructor properties are not available you have to do something more or less ugly.
Your code can be made more general by getting the static properties of any class.
A variant using type casting might be:
type ClassLike = { new(...args: readonly unknown[]): unknown }
type InstanceTypeWithConstructor<C extends ClassLike> =
{
constructor: C
}&
InstanceType<C>;
class Post {
static table = "Posts"
title = ""
}
const post = new Post() as InstanceTypeWithConstructor<typeof Post>;
const consTable = post.constructor.table;
playground link
The first version of my answer was erroneously removing prototype from the properties of constructor using key remapping
-- playground link

Accessing this inside super() method - javascript

The code is as follows
class ComposerForm extends BaseForm {
constructor(formsObject, options) {
super({
...options,
setup: {},
});
this.formsObject = { ...formsObject };
}
..
}
Now i have a new form
class PreferencesForm extends ComposerForm {
constructor(company, options = {}) {
super(
{
upids: new UpidsForm(company).initialize(),
featureSettings: new FeatureSettingsForm(company)
},
options
);
}
}
When initialising the FeatureSettingsForm, i need to pass the Preference form along with the company object
Something like
{
featureSettings: new FeatureSettingsForm(company, {prefForm: this})
},
so that i can access the preference form inside featureSettings form.
But this cannot be done since this cannot be accessed inside the super method.
Any idea on how to achieve this?
If I understand you right,
You need to pass a FeatureSettingsForm instance in the object you're passing to super (ComposerForm) in the PreferencesForm constructor, and
You need this in order to create the FeatureSettingsForm instance
So you have a circular situation there, to do X you need Y but to do Y you need X.
If that summary is correct, you'll have to¹ change the ComposerForm constructor so that it allows calling it without the FeatureSettingsForm instance, and add a way to provide the FeatureSettingsForm instance later, (by assigning to a property or calling a method) once the constructor has finished, so you can access this.
¹ "...you'll have to..." Okay, technically there's a way around it where you could get this before calling the ComposerForm constructor, by falling back to ES5-level ways of creating "classes" rather than using class syntax. But it general, it's not best practice (FeatureSettingsForm may expect the instance to be fully ready) and there are downsides to have semi-initialized instances (that's why class syntax disallows this), so if you can do the refactoring above instead, that would be better. (If you want to do the ES5 thing anyway, my answer here shows an example of class compared to the near-equivalent ES5 syntax.)

Using class object from diffrent class

I’m having type script class which expose some service list
I’ve defined a property that should have reference to the ServiceCatalog class like following:
export default class myGenerator extends Generator {
private svcat: ServiceCatalog | undefined;
// and here I’ve initilzied the property
await this.getSvc();
// here I created some function the return service instances
private async getSvc() {
this.svcat = new ServiceCatalog();
await this.svcat.getServiceInstances();
}
// and here I’ve additional code which use it
this.svcat.values ….
My question is there Is a better/cleaner way of doing the in javascript/typescript ?
maybe not using the this keyword...
And also maybe a better code for testing (unit-test) ...
The way you are doing today, it is very hard to test. Why is that? Well, because if you want to isolate your Generator class from your ServiceCatalog, you will have a hard time.
What I suggest, like the colleague above, is to have the ServiceCatalog coming by customer BUT have a default value.
class MyGenerator extends Generator {
constructor(private svCat: ServiceCatalog = new ServiceCatalog()) {
super();
}
}
This way you can use it normally like
new MyGenerator()
or for testing
new MyGenerator(myFakeServiceCatalog)
Inject the Service into your myGenerator class.
Add this to your constructor:
constructor(private svcat:ServiceCatalog) {}
You can now access the injected Service using
await this.svcat.getServiceInstances();
There is no need to add a property (your svcat:ServiceCatalog|undefined part) for the service.
"this" is needed a lot in java/type-script since it refers to the current class.

Access a class inside its static methods

I am building a simple class. My question is:
How to access a class method, or this for that matter, inside a static class method?
When trying to access this inside a static method like this:
const { ProductsCollection } = require('../utils/Collection')
let modeledCollection = ProductsCollection.mapToModel(legacy)
I get a TypeError: this.generateModel is not a function. Here is my class:
class ProductsCollection {
generateModel () {
let model = { name: 'testing' }
return model
}
static mapToModel (legacy) {
if (!isObject(legacy))
return legacy
let current = this.generateModel() // Here!!!
for (let key in legacy) {
// some code...
}
return current
}
}
module.exports = { ProductsCollection }
Thanks in advance!
How to access a class method, or this for that matter, inside a static class method?
The only way to access instance information from a static method is to create an instance (or receive one as a parameter, or close over one [which would be very odd], etc.; e.g., you need an instance). This is the point of static methods: They aren't associated with an instance of the class, they're associated with the constructor function.
Your generateModel method, as shown, doesn't use the instance either, so it may make sense for it to be static as well. Then you'd access it via this.generateModel (assuming mapToModel is called via ProductsCollection.mapToModel) or ProductsCollection.generateModel (if you don't want to make that assumption):
class ProductsCollection {
static generateModel() {
return {name: "testing"};
}
static mapToModel(legacy) {
return this.generateModel();
// or `return ProductsCollection.generateModel();` if you want to use
// `ProductsCollection` specifically and not be friendly
// to subclasses
}
}
console.log(ProductsCollection.mapToModel({}));
Or if generateModel need instance information, you might use new ProductsCollection (or new this) in your mapToModel to create an instance, then access generateModel on that instance.
class ProductsCollection {
generateModel() {
return {name: "testing"};
}
static mapToModel(legacy) {
const instance = new this();
// or `const instance = new ProductsCollection();` if you want
// to use `ProductsCollection` specifically and not be friendly
// to subclasses
return instance.generateModel();
}
}
console.log(ProductsCollection.mapToModel({}));
You cannot access an instance method as it were a static one. This is a classic error. The only way you can use an instance method inside a static one is if you have an instance of the class available (hence the name "instance method").
Usually you are making some mistake in how you are deciding what should be static and what not...without thinking much I can just suggest making the generateModel method static. Another way would be to remove the generateModel method completely and incorporate it's behaviour inside a constructor. It really depends on what your needs are.
Just remember that if you are accessing a non static property inside a method then it probably should not be static.

Is it possible to make constructor static in TypeScript?

I read about a static constructor in TypeScript and tried it myself but it doesn't work. I want to initialize a static variable by that (the method shall only be called once) but I get the following compiler error:
Error: 'static' modifier cannot appear on a constructor declaration.
Code:
export class DataManagement {
private static subjects: string[];
static constructor() {
DataManagement.subjects = [];
//some more code here
}
}
While other languages like C# do have static constructors, TypeScript (and JavaScript) do not have this feature. That said, you can still define a function statically which you invoke yourself. First, let me explain in which cases you might need a static constructor before I go over the options you have.
When to use a static constructor
A static constructor is useful in cases in which you need to calculate the value of a static attribute. If you just want to set an attribute (to a known value), you don't need a static constructor. It can be done like this:
class Example {
public static info = 123;
// ...
}
In case you need to compute the value, you have three ways to "simulate" a static constructor. I go over the options below and set the static attribute "info" in each sample.
Option 1: Call an initialize function after the class is declared
In the code below, the function _initialize is statically defined in the class and invoked immediately after the class is declared. Inside the function, this refers to the class, meaning that the keyword can be used to set any static values (like info in the sample below). Note, that it is not possible to make the function private, as it is invoked from outside.
class Example {
public static info: number;
public static _initialize() {
// ...
this.info = 123;
}
}
Example._initialize();
Option 2: Directly invoke the function inside the class
The second option is to use a function, which is directly called after its creation inside the class. The function only looks like its part of the class, but has no relation to the class itself (except being defined inside of it), meaning that you cannot use this inside the function.
class Example {
static info: number;
private static _initialize = (() => {
// "this" cannot be used here
Example.info = 1234;
})();
}
Alternative: Calculate a single attribute
Based on the same idea as option 2, you can calculate a single attribute by returning the value. This might be handy in cases where you only want to calculate one attribute of the class.
class Example {
public static info = (() => {
// ... calculate the value and return it
return 123;
})();
}
What to use
If you only want to calculate a single static attribute, you might want to use the last (alternative) approach. In case you need to do more complex calculations or want to set more attributes, I recommend using option 1 if you don't mind the function being public. Otherwise, go with option 2.
Note, that there is an issue in the TypeScript repository, that contains some more discussions regarding option 1 and 2.
Note that beginning with TypeScript 4.4, static block initializers are supported... https://devblogs.microsoft.com/typescript/announcing-typescript-4-4-rc/#static-blocks
class Foo {
static Foo.count = 0;
// This is a static block:
static {
if (someCondition()) {
Foo.count++;
}
}
}
Symbol() can be used for this, cleanly with restricted public access.
const STATIC_INIT = Symbol(); // gives you a unique identifier
class MyClass {
public static[STATIC_INIT] = () => {
// Your static init code here
}
}
// Call the init once
MyClass[STATIC_INIT]();
You can simulate static constructor using static code in your .ts file.
Let's say you have class which serves a single purpose of parsing parameters and merging them with some defaults.
Just add call after you class declaration.
Here is an example:
export class Env {
private static _args: {}
static get args() {
return this._args;
}
static _Initialize() {
// load settings from Environment
process.argv.forEach(s => console.log(s))
this._args = Object.assign({}, defaults, this.parseCmdLine())
}
}
Env._Initialize();
Example TS Application: https://github.com/v-andrew/ts-template
It works exactly as static constructor with who caveats:
It could be called multiple times
It could be called by other users of Env
To nullify those issues redefine _Initialize in the end of _Initialize:
this._Initialize = ()=>{}
You are looking for an object :) :
const dataManagement: { subjects: string[] } = {
subjects: []
};
export { dataManagement };
So the use of a static constructor is a bit of a misnomer. your are not try to make the constructor method static, but try to create a static instantiation method. It can be named whatever you want. I've used initialize, personally.
You can have your constructor method be essentially blank
constructor() {}
And then have a static initialize method
static initialize(): <type-to-use> { //initialization logic };
usually in the initialize method you want to invoke the constructor with the keyword new, and after that, default your properties.

Categories

Resources