Coders! I began to research JavaScript Proxy objects and faced some strange behaviour of one.
I'm trying to push a number to proxied array, but, unexpectedly for me, get method invoked, if it exists! However, if I delete get method in arrayHandler, set is invoked as expected, and number is pushed. Why is it possible?
class SomeClass {
public readonly name: String;
public originalData: Array<number> = [];
private arrayHandler = {
set(
target: Array<number>,
prop: number,
val: any,
reciever: any
): boolean {
console.log("PROXY HANDLER SET");
target[prop] = val;
return true;
},
get(target: Array<number>, prop: number): void {
console.log("PROXY HANDLER GET");
},
};
public proxyToArray: any = new Proxy(this.originalData, this.arrayHandler);
constructor(name: String) {
this.name = name;
}
}
let item = new SomeClass("item-1");
item.proxyToArray.push(1);
You need to return the target in the getter:
class SomeClass {
public readonly name: String;
public originalData: Array<number> = [];
private arrayHandler = {
set(
target: Array<number>,
prop: number,
val: any,
reciever: any
): boolean {
console.log("PROXY HANDLER SET");
target[prop] = val;
return true;
},
get(target: Array<number>, prop: number): any {
console.log("PROXY HANDLER GET");
return target[prop];
},
};
public proxyToArray: any = new Proxy(this.originalData, this.arrayHandler);
constructor(name: String) {
this.name = name;
}
}
let item = new SomeClass("item-1");
item.proxyToArray.push(1);
Related
I'm creating mini orm in my pet-project. Trying to do models usable in IDE and compiler, but stuck with typing problems.
I have base model class:
import { db } from '../db/db';
export interface Criteria<T> {
field: Extract<keyof T, string>;
value: T[Extract<keyof T, string>];
comparison?: string;
}
export class BaseModel {
static _pk = 'id';
static _table: string = null;
constructor(params: object) {
for (const key in params) {
if (Object.prototype.hasOwnProperty.call(params, key)) {
const fn = `${key}Process`;
if (typeof this[fn] === 'function') {
this[fn](params[key]);
} else {
this[key] = params[key];
}
}
}
}
create<T extends typeof BaseModel>(this: T): Promise<InstanceType<T>> {
const params = this._getParams(); //TS2339: Property '_getParams' does not exist on type 'T'.
const thisTyped = this.constructor as T;
return db
.insertFromModel(thisTyped._table, params)
.then((id) => thisTyped.findOne([{ field: thisTyped._pk, value: id }])); //TS2322: Type 'string' is not assignable to type 'Extract<keyof InstanceType<T>, string>'.
}
static findOne<T extends typeof BaseModel>(this: T, criteria: Criteria<InstanceType<T>>[]): Promise<InstanceType<T>> {
return db.selectFromModel(this._table, criteria, true).then((row: object) => {
return new this(row) as InstanceType<T>;
});
}
_getParams() {
const params = {};
for (const key of Object.keys(this)) {
if (['id', '_table', '_pk'].includes(key)) {
continue;
} else {
params[key] = this[key];
}
}
return params;
}
}
And child class:
import { BaseModel } from './BaseModel';
export class User extends BaseModel {
static _table = 'users';
static _pk = 'id';
id: number;
email: string;
password: string;
// eslint-disable-next-line #typescript-eslint/no-useless-constructor
constructor(props: { id?: number; email: string; password: string }) {
super(props);
}
}
static method from base class works good, but after adding create method I'm getting error while compile... Errors described in comments in code.
One more error could be achieved with code:
new User({ email: 'test#test.com', password: '123123' }).create().then((user) => {//TS2684: The 'this' context of type 'User' is not assignable to method's 'this' of type 'typeof BaseModel'. Type 'User' is missing the following properties from type 'typeof BaseModel': prototype, _pk, _table, findOne, and 2 more
console.log(user);
});
Don't understand what I'm doing wrong, I do all in analogy with static method.
Also full code for experiments available here - https://github.com/kricha/ts_q
thanks.
I have a simple class which creates classes from JSON objects. Here are the types. When I call map on the destructured SNode, it complains that an array of ANode [] is not assignable to ANode | BNode. My expectation is the map takes each object from the objects in the destructured children ANode array so I did not expect this error. Can someone explain why this is happening? When I hover over the { children } = snode in the IDE, it shows me that children is an array of ANode[], so why is map not passing individual ANodes? My goal is to be able to return a specific type when getClass determines the type of node passed in. As I am new to Typescript, I don't know how to do that to accomplish my goal.
interface ANode {
id: string;
value: string;
}
interface BNode {
id: string;
value: string;
}
interface SNode {
sid: string;
children: Array<ANode>;
}
function isSNode(node: any): node is SNode {
return node && "sid" in node;
}
class SObject {
constructor(public children: Map<string, ANode>) {}
}
type ValidNode = ANode | BNode;
function getClass(node: ValidNode): SObject {
if (isSNode(node)) {
return createSNodeClass(node);
}
}
function createSNodeClass(snode: SNode): SObject {
const { children = [] } = snode;
return new SObject(toMap(children.map(getClass))); //
//Error
//SNode [] is not assignable to getClass of ANode | BNode
}
function toMap(nodes: ANode[]) {
const m = new Map<string, ANode>();
for (const node of nodes) {
m.set(node.id, node);
}
return m;
}
class ANode {
constructor(
public id: string,
public value: string,){}
}
class BNode {
constructor(
public id: string,
public value: string,){}
}
class SNode{
constructor(
public sid: string,
public children: Array<ANode>,){}
}
function isSNode(node:any): node is SNode{
return node && node instanceof SNode;
}
function isANode(node:any): node is ANode{
return node && node instanceof ANode;
}
function isBNode(node:any): node is BNode{
return node && node instanceof BNode;
}
class SObject {
constructor(
public children: ANodeClass[]
){}
}
type ValidNode = ANode | BNode | SNode
type FinalOutput = ANodeClass | BNodeClass | SObject
class ANodeClass {
constructor(
public node: ANode){
this.id = node.id;
this.value = node.value;
}
id: string;
value: string;
}
class BNodeClass {
constructor(
public node: BNode){}
id: string;
value: string;
}
function getClass(node: ValidNode): FinalOutput | undefined{
if(isSNode(node)){
return createSNodeClass(node)
}else if(isANode(node)){
return new ANodeClass(node)
}else if(isBNode(node)){
return new BNodeClass(node)
}else{
throw new Error('Not Allowed type');
}
}
function createSNodeClass(snode:SNode): SObject{
const {children = []} = snode;
return new SObject(children.map(node =>
getClass(node) as ANodeClass
));
}
function toMap(nodes: ANodeClass []){
const m = new Map<string, ANodeClass>();
for(const node of nodes){
m.set(node.id, node);
}
return m;
}
I have a problem with typescript. The problem is that it suddenly stopped compiling class properties to the .js file. It compiles everything, the functions the constructor but the properties aren't there.
Server.ts
export class Server{
id: String;
prefix: String;
constructor(){
}
copy(object){
object = JSON.parse(JSON.stringify(object));
console.log(this);
for (const k in object) {
if(this.hasOwnProperty(k))
this[k] = object[k];
}
console.log(this);
}
}
Server.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Server = void 0;
class Server {
constructor() {
}
copy(object) {
object = JSON.parse(JSON.stringify(object));
console.log(this);
for (const k in object) {
if (this.hasOwnProperty(k))
this[k] = object[k];
}
console.log(this);
}
}
exports.Server = Server;
Scenario.ts
import { percentageChance } from "../utilities/percentageChance";
import { Champion } from "./Champion";
export class Scenario {
name: String;
description: String;
champion: Champion;
choices: string[];
outcome: number[];
final: string[];
constructor(name: String, description: string | String, champion: Champion, choices: string[], outcome: number[], final: string[]) {
this.name = name;
this.description = description;
this.champion = champion;
this.choices = choices;
this.outcome = outcome;
this.final = final;
}
calculateOutcome(choiceIndex : number) {
if(percentageChance(this.choices, this.outcome) == this.choices[choiceIndex]){
return true;
}
return false;
}
}
Scenario.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Scenario = void 0;
const percentageChance_1 = require("../utilities/percentageChance");
class Scenario {
constructor(name, description, champion, choices, outcome, final) {
this.name = name;
this.description = description;
this.champion = champion;
this.choices = choices;
this.outcome = outcome;
this.final = final;
}
calculateOutcome(choiceIndex) {
if (percentageChance_1.percentageChance(this.choices, this.outcome) == this.choices[choiceIndex]) {
return true;
}
return false;
}
}
exports.Scenario = Scenario;
Has anyone had anything like this happened? If yes then how should it fix this?
I think that one is easy, as JS is a dynamically typed language no property that was not assigned a value will be created. So typescript will leave those properties to be created on-demand.
If you put a default value in class properties, it makes them transpile to JS on compile time.
export class Scenario {
name: String = '';
description: String = '';
champion: Champion = new Champion();
choices: string[] = [];
outcome: number[] = [];
final: string[] = [];
//...
}
Had the same problem using a method to copy properties of the same values from another object, since my properties do not exist (because of absent default value) nothing was copied...
I want to create a domain model correctly.
My attempt below creates the properties outside the constructor.
Should I be creating and setting the properties of the TestModel class inside the constructor only? this way their would be less lines of code
I have the below attempt that I think is correct:
export class TestModel1 {
public currentPage: number = 0;
public hasNext: boolean = false;
public hasPrev: boolean = false;
public pageSize: number = 0;
public totalItems: number = 0;
constructor(data: any) {
this.currentPage = data.currentPage;
this.hasNext = data.hasNext;
this.hasPrev = data.hasPrev;
this.pageSize = data.pageSize;
this.totalItems = data.totalItems;
}
}
It just seems a little big, too many lines of code.
Currently I need to pass in a data object and then map.
Is their a clever way for me to implement this better using the constructor function?
If we speak about the model class, the declaration of that should be like in example below:
export class Account {
constructor(
public activated: boolean,
public authorities: string[],
public email: string,
public firstName: string,
public langKey: string,
public lastName: string,
public login: string,
public imageUrl: string
) {}
}
Certainly you should not to define values outside of constructor. You may do declare the model class members as you have in your example, but without the definition of values:
export class TestModel1 {
public currentPage: number;
public hasNext: boolean;
public hasPrev: boolean;
public pageSize: number;
public totalItems: number;
constructor(data: any = null) {
if(data !== null) {
this.currentPage = data.currentPage;
this.hasNext = data.hasNext;
this.hasPrev = data.hasPrev;
this.pageSize = data.pageSize;
this.totalItems = data.totalItems;
}
}
}
And if you want to declare default values, my advice is to do this inside the constructor for clean and good code.
Update:
export class TestModel1 {
public currentPage: number;
public hasNext: boolean;
public hasPrev: boolean;
public pageSize: number;
public totalItems: number;
constructor(data: any = null) {
if(data !== null) {
this.currentPage = data.currentPage;
this.hasNext = data.hasNext;
this.hasPrev = data.hasPrev;
this.pageSize = data.pageSize;
this.totalItems = data.totalItems;
}
else {
this.currentPage = 0;
this.hasNext = false;
this.hasPrev = false;
this.pageSize = 0;
this.totalItems = 0;
}
}
}
That's would be better, if you want to have defaults
The main issue I see in the question is that of duplication. All of the properties are duplicated in the constructor, which violates the DRY principle of clean code.
If you want to populate a new instance of your domain object in a more concise way, you can do the following:
export class TestModel1 {
public currentPage: number;
public hasNext: boolean;
public hasPrev: boolean;
public pageSize: number;
public totalItems: number;
constructor(data: any) {
Object.keys(data).forEach(key => {
this[key] = data[key]
});
}
}
You have to ensure that the input data object will have the correct properties only, which may or may not be easy, based on the source of data.
Using Angular & TypeScript, we can use generics and all the Compile-goodness to assure some sort of type-safety. But if we are using for example the HTTP-Service, we don't get a specific objec but just parsed JSON. For example, we have some generic methods doing that:
public get<T>(relativeUrl: string): Promise<T> {
const completeUrlPromise = this.createCompleteUrl(relativeUrl);
const requestOptions = this.createRequestOptions(ContentType.ApplicationJson, true);
return completeUrlPromise.then(completeUrl => {
return this.processResponse<T>(this.http.get(completeUrl, requestOptions));
});
}
private processResponse<T>(response: Observable<Response>): Promise<T> {
const mappedResult = response.map(this.extractData);
const result = mappedResult.toPromise();
return result;
}
private extractData(res: Response): any {
let body;
if (!Array.isArray(res)) {
if (res.text()) {
body = res.json();
}
} else {
body = res;
}
if (!JsObjUtilities.isNullOrUndefined(body)) {
return body;
}
return {};
}
Ultimately, the generic type is useless this way, since we just get the JSON. If the generic object has methods or properties not in the JSON, they are lost.
To avoid this, we added the possibility to pass a constructor-function to truly create the object:
private processResponse<T>(response: Observable<Response>, ctor: IParameterlessConstructor<T> | null = null): Promise<T> {
let mappedResult = response.map(this.extractData);
if (ctor) {
mappedResult = mappedResult.map(f => {
const newObj = JsObjFactory.create(f, ctor);
return newObj;
});
}
const result = mappedResult.toPromise();
return result;
}
And the JsObjFactory looking like this:
export class JsObjFactory {
public static create<T>(source: any, ctorFn: IParameterlessConstructor<T>): T {
const result = new ctorFn();
this.mapDefinedProperties(source, result);
return result;
}
private static mapDefinedProperties<T>(source: Object, target: T): void {
const properties = Object.getOwnPropertyNames(target);
properties.forEach(propKey => {
if (source.hasOwnProperty(propKey)) {
target[propKey] = source[propKey];
}
});
}
}
This works well for shallow objects, but doesn't work, if a property is also a complex type with a constructor. As there are no types at runtime, the best bet I have currently is to kindahow parse the properties, check if classes exist and then create them. But this seems to be very error-prone and cumbersome.
Since I'm always certain, I'm not the only person with this issues, are there solutions, or TypeScript/JavaScript features I'm not aware off, which would help here?
I don't personally do it like this, but it may be what you're looking for.
Example:
Customer.ts
export interface ICustomer {
Id: number;
Name: string;
Orders: IOrder[];
...
}
export class Customer implements ICustomer {
public Id: number;
public Name: string;
public Orders: IOrder[];
constructor(customer: Partial<ICustomer>) {
this.Id = customer.Id || 0;
this.Name = customer.Name || '';
this.Orders = [];
customer.Orders.forEach((order: IOrder) => this.Orders.push(new Order(order)));
}
//some functions
}
Order.ts
export interface IOrder {
Id: number;
Weight: number;
Shipmentdate: string;
}
export class Order implements IOrder {
public Id: number;
public Weight: number;
public Shipmentdate: string;
constructor(order: Partial<IOrder>) {
this.Id = order.Id || 0;
this.Weight = order.Weight || 0;
this.Shipmentdate = order.Shipmentdate || '';
}
//some functions
}
This would make the Object (in this case Customer) responsible for instantiating it's known complex types that you pass in. And Order in turn could have its complex types that it instantiates.