Imagine this trivial custom element:
<my-el data-cute-number="7" id="foo"></my-el>
document.getElementById('foo').dataset.cuteNumber, as expected, returns the String "7". I would like to create a proxy for accessing dataset properties that does the casting to Number for me because I'm using the property alot in the component code and would like to avoid repeatedly having to cast it manually every time I access it. I also do not want to create an additional getter for a new property (e.g. get cuteNumber() { return Number(this.dataset.cuteNumber); }) on the component itself since I will have to do all the synchronisation manually then (since I'd also need a setter), make sure I avoid infinite update loops, etc.
As I understand proxies, this is exactly where a proxy will help me.
customElements.define('my-el', class extends HTMLElement {
constructor() {
super();
const proxy = new Proxy(this.dataset, {
get: function(context, prop, receiver) {
console.log(`Proxy getter executing for ${prop}`);
switch (prop) {
case 'cuteNumber':
return Number(context[prop]);
break;
default:
return context[prop];
}
}
});
}
})
console.log(typeof document.getElementById('foo').dataset.cuteNumber);
<my-el data-cute-number="7" id="foo"></my-el>
This is where I'm stuck.
Accessing the dataset currently does not trigger the proxy (the inner console.log doesn't show up).
Can anyone point me in the right direction? Is it even possible to proxy the dataset of an element?
Creating a proxy does not mutate the target object, it doesn't become a proxy. The proxy is a new object that wraps around the target. In your code, you are just throwing away the proxy and never use it - the .dataset property is unaffected. You'll want to either overwrite it or create a new property:
customElements.define('my-el', class extends HTMLElement {
get dataset() {
//^^^^^^^^^^^^^
return new Proxy(super.dataset, {
// ^^^^^^
get: function(target, prop, receiver) {
console.log(`Proxy getter executing for ${prop}`);
if (prop == 'cuteNumber')
return Number(target.cuteNumber);
return Reflect.get(target, prop, receiver);
}
});
}
});
console.log(typeof document.getElementById('foo').dataset.cuteNumber);
<my-el data-cute-number="7" id="foo"></my-el>
customElements.define('my-el', class extends HTMLElement {
constructor() {
super();
this.numberdata = new Proxy(this.dataset, {
// ^^^^^^^^^^^^^^^^^
get: function(target, prop, receiver) {
return Number(target[prop]);
}
});
}
});
console.log(typeof document.getElementById('foo').numberdata.cuteNumber);
<my-el data-cute-number="7" id="foo"></my-el>
Related
I want to build a class that can compose multiple objects and use any of their interfaces.
Class A can use any of the interfaces of Class B and C
B can use any of the interfaces of C
C can use any of the interfaces of B
I have the above functionality written in JavaScript and I was wondering what's the best and correct way to achieve the same using TypeScript:
import { findLast, isFunction } from "lodash";
class Composite {
constructor(behavior) {
this.behaviors = [];
if (behavior) {
this.add(behavior);
}
}
add(behavior) {
behavior.setClient(this);
this.behaviors.push(behavior);
return this;
}
getMethod(method) {
const b = findLast(this.behaviors, (behavior) =>
isFunction(behavior[method])
);
return b[method].bind(b);
}
}
class Behavior1 {
foo() {
console.log("B1: foo");
}
foo2() {
console.log("B1: foo2");
this.getMethod("bar")();
}
setClient(client) {
this.client = client;
}
getMethod(method) {
return this.client.getMethod(method);
}
}
class Behavior2 {
foo() {
console.log("B2: foo");
this.getMethod("foo2")();
}
bar() {
console.log("B2: bar");
}
setClient(client) {
this.client = client;
}
getMethod(method) {
return this.client.getMethod(method).bind(this);
}
}
const c = new Composite();
c.add(new Behavior1());
c.add(new Behavior2());
c.getMethod("foo")();
c.getMethod("bar")();
// Output:
// B2: foo
// B1: foo2
// B2: bar
// B2: bar
Link to codesandbox: https://codesandbox.io/s/zen-poitras-56f4e?file=/src/index.js
You can review my other answer to see some of the issues and concerns with the previous approach. Here I've created a completely different version from the ground up. There is less code repetition and less tight coupling between the classes.
Behaviors no longer call methods directly and no longer store a reference to the client. Instead, they receive the client (or any object which call get and call methods) as an argument of their register method.
We define any object which can lookup and call methods as a MethodAccessor
interface MethodAccessor {
getMethod(name: string): () => void;
safeCallMethod(name: string): boolean;
}
We define any object that provides behaviors through a register method as a BehaviorWrapper. These objects can call functions from other objects by calling getMethod or safeCallMethod on the helper argument.
type KeyedBehaviors = Record<string, () => void>;
interface BehaviorWrapper {
register(helper: MethodAccessor): KeyedBehaviors;
}
A behavior which does not need instance variables could be a pure function rather than a class.
const functionBehavior = {
register(composite: MethodAccessor) {
return {
foo: () => console.log("B1: foo"),
foo2: () => {
console.log("B1: foo2");
composite.safeCallMethod("bar");
}
};
}
};
Class behaviors can make use of instance variables in their methods.
class ClassBehavior {
name: string;
constructor(name: string) {
this.name = name;
}
bar = () => {
console.log(`Hello, my name is ${this.name}`);
};
register() {
return {
bar: this.bar
};
}
}
There is some redundancy here when defining a method like bar separately rather than inline as an arrow function within the return object. The reason that I am having the methods come from register rather than using all class methods is so that I can have stricter typing on them. You could have methods in your class which do require args and as long as they aren't part of the register returned object then it's not a problem.
Our class Composite now stores its behaviors in a keyed object rather than an array. Newly added behaviors of the same name will overwrite older ones. Our getMethod is typed such that it always returns a method, and will throw an Error if none was found. I've added a new method safeCallMethod to call a method by name. If a method was found, it calls it and returns true. If no method was found, it catches the error and returns false.
class Composite implements MethodAccessor {
behaviors: KeyedBehaviors = {};
constructor(behavior?: BehaviorWrapper) {
if (behavior) {
this.add(behavior);
}
}
// add all behaviors from a behavior class instance
add(behavior: BehaviorWrapper): this {
this.behaviors = {
...this.behaviors,
...behavior.register(this)
};
return this;
}
// lookup a method by name and return it
// throws error on not found
getMethod(method: string): () => void {
const b = this.behaviors[method];
if (!b) {
throw new Error(`behavior ${method} not found`);
}
return b;
}
// calls a method by name, if it exists
// returns true if called or false if not found
safeCallMethod(method: string): boolean {
try {
this.getMethod(method)();
return true;
} catch (e) {
return false;
}
}
}
There's a lot that's not ideal about your setup. I might post a separate answer with an alternate setup, but for now I just want to show you how to convert your code to typescript.
Keep in mind that typescript errors exist to help you prevent runtime errors, and there are some genuine potential runtime errors that we need to avoid. If a Behavior calls getMethod before calling setClient to set this.client that will be a fatal error. If you try to call the returned method from getMethod on a Composite or a Behavior where the name didn't match a method that's another fatal error. And so on.
You choose to handle certain situations by throwing an Error with the expectation that it will be caught later on. Here I am preferring to "fail gracefully" and just do nothing or return undefined if we can't do what we want. The optional chaining ?. helps.
When defining an interface for a function argument, it's best to keep it to the minimum necessities and not require any extraneous properties.
The only thing that a Behavior requires of its Client is a getMethod method.
interface CanGetMethod {
getMethod(name: string): MaybeMethod;
}
We use the union of undefined and a void function in a few places, so I am saving it to an alias name for convenience.
type MaybeMethod = (() => void) | undefined;
The Composite calls setClient on its behaviors, so they must implement this interface.
interface CanSetClient {
setClient(client: CanGetMethod): void;
}
It also expects that its methods take zero arguments, but we can't really declare this with the current setup. It is possible to add a string index to a class, but that would conflict with our getMethod and setClient arguments which do require arguments.
One of the typescript errors that you get a bunch is `Cannot invoke an object which is possibly 'undefined', so I created a helper method to wrap a function call.
const maybeCall = (method: MaybeMethod): void => {
if (method) {
method();
}
};
In typescript, classes need to declare the types for their properties. Composite gets an array of behaviors behaviors: CanSetClient[]; while the behaviors get a client client?: CanGetMethod;. Note that the client must be typed as optional because it is not present when calling new().
After that, it's mostly just a matter of annotating argument and return types.
I have declared the interfaces that each class implements, ie. class Behavior1 implements CanGetMethod, CanSetClient, but this is not required. Any object fits the interface CanGetMethod if it has a getMethod property with the right types, whether it explicitly declares CanGetMethod in its type or not.
class Composite implements CanGetMethod {
behaviors: CanSetClient[];
constructor(behavior?: CanSetClient) {
this.behaviors = [];
if (behavior) {
this.add(behavior);
}
}
add(behavior: CanSetClient): this {
behavior.setClient(this);
this.behaviors.push(behavior);
return this;
}
getMethod(method: string): MaybeMethod {
const b = findLast(this.behaviors, (behavior) =>
isFunction(behavior[method])
);
return b ? b[method].bind(b) : undefined;
}
}
class Behavior1 implements CanGetMethod, CanSetClient {
client?: CanGetMethod;
foo() {
console.log("B1: foo");
}
foo2() {
console.log("B1: foo2");
maybeCall(this.getMethod("bar"));
}
setClient(client: CanGetMethod): void {
this.client = client;
}
getMethod(method: string): MaybeMethod {
return this.client?.getMethod(method);
}
}
class Behavior2 implements CanGetMethod, CanSetClient {
client?: CanGetMethod;
foo() {
console.log("B2: foo");
maybeCall(this.getMethod("foo2"));
}
bar() {
console.log("B2: bar");
}
setClient(client: CanGetMethod) {
this.client = client;
}
getMethod(method: string): MaybeMethod {
return this.client?.getMethod(method)?.bind(this);
}
}
const c = new Composite();
c.add(new Behavior1());
c.add(new Behavior2());
maybeCall(c.getMethod("foo"));
maybeCall(c.getMethod("bar"));
I have the following TypeScript class that contains a getter / setter:
export class KitSection {
uid: string;
order: number;
set layout(layout: KitLayout) {
this._layout = new KitLayout(layout);
}
get layout() {
return this._layout;
}
private _layout: KitLayout;
constructor(init?: Partial<KitSection>) {
Object.assign(this, init);
}
}
// I can create an instance like so:
const section = new KitSection(data);
I need to POST this instance to my server as a JSON object to store in a MySQL database column of type: JSON, so I figured I could do the following:
const jsonSection = JSON.parse(JSON.stringify(section))
This creates a JSON object, but when I inspect in the console, i see my private getter/setter variable instead of the public variable in the object:
console.log(jsonSection);
///IN CONSOLE///
uid: "a"
order: 0
_layout: {_rows: Array(2), columns: 12}
I don't want to store the private variable _layout in my database, I need to store the public counterpart defined in the getter/setter: layout.
Next, I checked out the answer provided here: https://stackoverflow.com/a/44237286/6480913 where it suggests to add the following method to convert to JSON:
public toJSON(): string {
let obj = Object.assign(this);
let keys = Object.keys(this.constructor.prototype);
obj.toJSON = undefined;
return JSON.stringify(obj, keys);
}
However, this returns an empty object. Upon inspection, when I console.log() the this.constructor.prototype, I see the all the properties of the object, but each object is kind of greyed out, so when we use Object.keys(), I receive an empty array. Why are these constructor properties greyed out?
JSON.stringify will only iterate over own enumerable properties. Here, since layout is a property on the prototype object, not the instance itself, when you try to stringify the instance, the getter is not invoked. (but since the _layout is an own enumerable property, it is included in the result)
Similarly, the following stringified object is empty:
const obj = Object.create({
get prop() {
return 'val';
}
});
console.log(JSON.stringify(obj));
You could fix it by putting the getter directly on the instance, and by making the _layout non-enumerable. This way, when stringified, the getter will be invoked, but _layout will not be included:
export class KitSection {
uid: string;
order: number;
private _layout: KitLayout;
constructor(init?: Partial<KitSection>) {
Object.defineProperty(
this,
'layout',
{
enumerable: true,
get() {
return this._layout;
},
set(newVal) {
this._layout = new KitLayout(newVal);
}
}
);
Object.defineProperty(
this,
'_layout',
{
enumerable: false,
value: undefined,
}
);
Object.assign(this, init);
}
}
const section = new KitSection(data);
It'll look a bit clearer if you use private class field syntax instead:
export class KitSection {
#layout: KitLayout | undefined;
constructor(init?: Partial<KitSection>) {
Object.defineProperty(
this,
'layout',
{
enumerable: true,
get() {
return this.#layout;
},
set: (newVal) => {
this.#layout = new KitLayout(newVal);
}
}
);
Object.assign(this, init);
}
}
You could also invoke the getter manually.
If the KitLayout is essentially serializable, to turn the serialized object back into a KitSection instance, KitLayout or one of its helper methods needs to be able to turn a serialized KitLayout into an instance. Either pass it through the constructor again (just call new KitSection, which hands it to KitLayout), or separate the layout from the deserialized object and have a separate method on KitSection that calls KitLayout's helper method and assigns the private property, maybe something like:
integrateLayout(layoutInfo) {
this.#layout = KitLayout.makeKitLayoutFromLayoutInfo(layoutInfo)
}
where layoutInfo is the plain object.
I have some dynamically generated function names in TypeScript. The only way I can use them now is to cast my objects to <any>. Ex.: <any>myInstance.getDataA(). These functions are dynamically generated based on some rules. Based on the same rules I'd like to generate type-definitions for my classes, but I can not make it work.
original.ts
abstract class Original {
dynamics = ['getData', 'setData'];
constructor() {
// I create functions here dynamically
this.dynamics.forEach((key) => {
this[key + this.info] = () => null;
});
}
get info() {
return 'X';
}
}
my-class.ts
class MyClass extends Original {
get info() {
return 'A';
}
}
my-other-class.ts
class MyOtherClass extends Original {
get info() {
return 'B';
}
}
something.ts
const myInstance = new MyClass();
console.log(myInstance.getDataA()); // TS2339: Property getDataA does not exist on type: 'MyClass'
const myOtherInstance = new MyOtherClass();
console.log(myInstance.getDataB()); // TS2339: Property getDataB does not exist on type: 'MyClass'
I would like to automatically generate a definition file to define these dynamic properties.
Ex.:
my-class.def.ts
declare interface MyClass {
getDataA;
setDataA
}
//my-other-class.def.ts
declare interface MyClass {
getDataB;
setDataB
}
But I can not find a syntax for my definition files to make it work. Pls ask me if I was not clear, and pls help if you have any idea!
Edit for 4.1
Using Template literal types and mapped type 'as' clauses we can now do concatenate strings in the type system and create a class that has these properties created dynamically.
function defineDynamicClass<T extends string[]>(...info: T): {
new (): {
[K in T[number] as `get${Capitalize<K>}`]: () => unknown
} & {
[K in T[number] as `set${Capitalize<K>}`]: (value: unknown) => void
} & {
info: T
}
} {
return class {
get info () {
return info;
}
} as any
}
class MyClass extends defineDynamicClass('A', 'B', 'ABAB') {
}
let s =new MyClass();
s.getA();
s.getABAB();
s.setA("")
s.info;
Playground Link
Before 4.1
The within language approach
There is no way to do this within the type system, since we can't perform string manipulation on string literal types. The closest you can get, without external tools, is to create get/set methods that take a string literal type, that will be of the same as that returned by the getInfo method.
function stringLiteralArray<T extends string>(...v: T[]){ return v;}
abstract class Original {
get(name: this['info'][number]) {
return null;
}
set(name: this['info'][number], value: any) {
return null;
}
get info() : string[]{
return [];
}
}
class MyOtherClass extends Original {
get info() {
return stringLiteralArray('A', 'B', 'ABAB');
}
}
class MyClass extends Original {
get info() {
return stringLiteralArray('C', 'D', 'DEDE');
}
}
let s =new MyClass();
s.get('A') // error
s.get('C') // ok
While this approach is not 100% what you want, form our previous discussions the aim was to have full code-completion for the methods, and this approach achieves this. You get errors if you pass in the wrong value and you get a completion list for the string:
The compiler API approach
A second approach would be to create a custom tool that uses the typescript compiler API to parse the ts files, look for classes derived from Original and generates interfaces containing the methods (either in the same file or a different file) , if you are interested in this I can write the code, but it's not trivial, and while the compiler API is stable I don't think the compiler team takes as much care with backward compatibility as they do with the language (in fact this is the exact statement they make in the documentation page).
If you are interested in such a solution, let me know and I can provide it, but I advise against it.
I am trying to use a Proxy, and I am having issues. I have a class like so:
export class Builder {
public doSomething(...args: (string | number | Raw | Object)[]): this {
// Do stuff
return this
}
}
export class ModelBase extends Builder {
protected _items = {}
}
export class Model extends ModelBase {
public constructor(options?: ModelSettings) {
super(options)
return new Proxy(this, {
get: function (target, property) {
return target._items[property] || target
}
})
}
public static create() {
return new this()
}
}
I then extend Model like so:
export class MyClass extends Model {
public constructor() {
super({/* Some options go here */})
// Do some stuff
}
public static getItems() {
let t = this.create()
t.doSomething()
}
}
Then I call getItems() which creates an instance of the class.
MyClass.getItems()
When I run this, I get the error:
TypeError: t.doSomething is not a function
Where doSomething() is within the class ModelBase. If I comment out the Proxy things work as usual. So, I would like to know why I can't access the parent class.
Your proxy is trying to find target._items.doSomething, but that doesn't exist. target.doSomething does exist, but it's not looking for that. As a result, it falls back to returning target, which is the object as a whole. The object, is not a function, and thus the error.
As for how to fix this, that depends on what you're trying to achieve with the proxy. If there's a limited set of properties that the proxy should be paying attention to, you could check those explicitly. Or more generally you might be able to check whether target[property] exists, then fall back to target._items[property] and only then fall back to target. Again, it depends on what you're trying to achieve.
I have a JavaScript object which dynamically allows members to be bound as accessor properties to instances of the object:
Source
function DynamicObject(obj) {
for (var prop in obj) {
Object.defineProperty(this, prop, {
get: function () { return obj[prop]; },
set: function (value) { obj[prop] = value; },
enumerable: true,
configurable: false
});
}
}
Usage
var obj = new DynamicObject({
name: "John Smith",
email: "john.smith#test.net",
id: 1
});
When obj is created, the members of the constructor parameter are bound to obj as accessor properties. These show up in intellisense
I would like to know if it is possible to model this sort of behavior (including having intellisense) in TypeScript?
Notes
When you run this code in TypeScript, there is no intellisense becuase everything is any, so TypeScript doesn't really know what's going on.
You can't. These are completely dynamic properties, added at runtime, so you can't possibly know what they are at compile-time. I would also argue that you don't want to know what they are that early; if you have constraints to enforce, they should just be stated on their own (first example below).
If your code depends on a set of accessors, you should put those in the interface or contract directly, because you know ahead of time that you expect them and should advertise that. You can use optional properties (with the accessor defined lower) to make that simpler:
interface HasSomeProps {
foo: string;
bar?: string;
}
class DoesTheProps implements HasSomeProps {
set foo(value) {
// ...
}
}
If you have a bunch of consistent (or semi-consistent) accessors, you can define an indexer on your type like:
interface AccessStrings {
[key: string]: string;
}
That does not allow you to restrict the keys. If you want that, you should explicitly list the properties.
I would like to know if it is possible to model this sort of behavior (including having intellisense) in TypeScript?
Yes.
You can assign a generic call signature to DynamicObject. You'll need to declare it as a variable:
var DynamicObject: new <T>(obj: T) => T = function (obj)
{
for (var prop in obj)
{
Object.defineProperty(this, prop, {
get: function () { return obj[prop]; },
set: function (value) { obj[prop] = value; },
enumerable: true,
configurable: false
});
}
} as any;
This way, IntelliSense will treat the value returned from new DynamicObject as having the same type as the value passed in. Same property names, same property types. You'll get full autocomplete and type-safety.
Live demo on TypeScript Playground
If you have trouble wrapping your head around that part in the first line, it's the same as writing the following:
// Declare type (only exists during compile-time)
var DynamicObject: new <T>(obj: T) => T;
// Assign value (during runtime)
DynamicObject = function (obj)
{
...
} as any;