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.
Related
// inital types
interface Source {
id: string;
version: number; // discard
masterVariant: MasterVariant;
}
interface MasterVariant {
id: number; // discard
sku?: string;
}
// desired "lighter" types
interface Target {
id: string;
masterVariant: MasterVariantLight;
}
interface MasterVariantLight {
sku?: string;
}
to remove version property we can use the following
export class Convert {
public static toTarget(source: Source): Target {
const { version, ...result } = source;
return result;
}
}
can you see a way to discard masterVariant.id using above transformation?
export class Convert {
public static toTarget(source: Source): Target {
const { version, masterVariant: { id, ...rest }, ...result } = source;
return { id: result.id, masterVariant: { ...rest } };
}
}
Playground
While pure destructuring is possible here, I think it'd be clearer to use one more line to construct the required masterVariant format.
Having a class that never gets instantiated doesn't make much sense either - a plain function would be more suitable for the task (which is fine, unlike in Java).
const toTarget = (source: Source) => {
const { masterVariant, ...origSource } = source;
const newMasterVariant = { sku: masterVariant.sku };
return { ...origSource, masterVariant: newMasterVariant };
};
I have a class with two private methods:
export class LayerEditor {
public layerManager: LayerManager;
constructor() {
this.layerManager = new LayerManager(this);
}
private executeCommand() {
switch (this.layerManager.editableLayer.type) {
case LayerType.common:
this.commandManager.set(new SelectExistCommand(this.reonMap));
break;
case LayerType.point:
this.commandManager.set(new SelectExistPointCommand(this.reonMap));
break;
}
}
private createTools(): EditorTools {
switch (this.layerManager.editableLayer.type) {
case LayerType.common:
return new CommonTools(new CommonToolsFactory());
case LayerType.point:
return new PointTools(new PointToolsFactory());
}
}
}
How to avoid two switches no joining two methods in one switch? How to apply flexibility of OOP?
Class LayerManager manages a layers:
class LayerManager {
public selectedLayer: TreeNode;
public editableLayer: Layer;
public polygonsArea: string;
private readonly layers: TreeNode[];
constructor(private layerEditor: LayerEditor) {
this.layers = this.layerEditor.r.layersManager.editableLayers;
}
getLayers() {
return this.layers;
}
selectLayer(layerId: string) {
this.selectedLayer = { ...this.layerEditor.r.layersManager.getLayerConfig(layerId), enabled: true };
this.layerEditor.r.state.changeTreeNode(this.selectedLayer);
}
createLayerObject() {
this.editableLayer = FactoryLayer.create(this.selectedLayer.id, this.selectedLayer.type, this.layerEditor.r);
}
}
Command manager class makes works with commands. This class works as dispatcher and stores all commands.
class CommandManager {
public currentCommand: Command;
protected commands: Command[] = [];
protected undoCommand: Command;
constructor(public layerEditor: LayerEditor) {}
set(command: Command): void {
if (command === this.currentCommand) return;
this.currentCommand = command;
this.commands.push(this.currentCommand);
}
execute(): void {
this.currentCommand.execute();
}
undo(): void {
this.currentCommand.undo();
}
redo(): void {
this.currentCommand.redo();
}
cancel(): void {
while (this.commands.length) {
this.currentCommand = this.commands.pop();
this.undo();
}
}
complete(): void {
this.currentCommand.complete();
}
}
If you need anything else I will post
Essentially what you want to do is move all split decisions into one place. Instead of having many pairs of classes like PointTools, PointToolsFactory, SelectExistPointCommand, etc. you have one single PointConfig. You want your two configurations to match the same interface as much as possible. They both create a command and some tools. Other classes should be able to use the returned tools interchangeably without caring if it is PointTools or CommonTools. That way you can just call this.config.createTools().
This is the Factory Method Pattern and the C# Example should be readable as it's extremely similar to TypeScript. I've seen more of your code than what is posted here so I know that you were trying to do this already with a class FactoryLayer. But you want to define it so that the factory is the only switch that you need.
It should be something roughly along these lines:
interface LayerConfig {
id: string;
type: SemanticLayerType;
fields: Record<string, FieldConfig>;
createTools(): Button[];
createSelectCommand(map: ReonMap): Command;
}
class PointConfig implements LayerConfig {
readonly type = SemanticLayerType.Point;
readonly fields = {}; // fill this in
constructor(public readonly id: string) {}
createTools() {
return []; // fill this in
}
createSelectCommand(map: ReonMap): Command {
// fill this in
}
}
class PolygonConfig implements LayerConfig {
readonly type = SemanticLayerType.Polygon;
readonly fields = {}; // fill this in
constructor(public readonly id: string) {}
createTools() {
return []; // fill this in
}
createSelectCommand(map: ReonMap): Command {
// fill this in
}
}
class Layer {
tools: Button[];
constructor(public readonly config: LayerConfig) {
this.tools = config.createTools();
}
// other stuff
}
export class FactoryLayer {
static create(layerId: string, layerType: LayerType): Layer {
if (layerType === LayerType.common) {
return new Layer(new PolygonConfig(layerId));
}
if (layerType === LayerType.point) {
return new Layer(new PointConfig(layerId));
}
throw new Error("Unknown Layer Type " + layerType);
}
}
export class LayerEditor {
public layerManager: LayerManager;
constructor(private map: ReonMap, private commandManager: CommandManager) {
this.layerManager = new LayerManager(map); //this);
}
private getCurrentLayer(): Layer {
if (!this.layerManager.editableLayer) {
throw new Error("No Layer Selected");
}
return this.layerManager.editableLayer;
}
private executeCommand(): void {
// you could probably clean this up but you get the idea
this.commandManager.execute(
this.getCurrentLayer().config.createSelectCommand(this.map)
);
}
public getTools(): Button[] {
return this.getCurrentLayer().tools;
}
}
I have just tried to bring common things together. And trying to leverage Typescript features.
In both the private method you have this.layerManager.editableLayer.type common, now by adding an optional parameter and making use of returning multiple datatype. Below is the updated code.
If need you can default parameter as type = 'execute' and whenever needed pass parameter as create
private checkTool( type?: string): EditorTools | void {
const layerManagerType = this.layerManager.editableLayer.type
switch(layerManagerType) {
case LayerType.common:
return (type === 'execute') ?
this.commandManager.set(new SelectExistCommand(this.reonMap))
: new CommonTools(new CommonToolsFactory());
break;
case LayerType.point:
return ( type === 'execute') ?
this.commandManager.set(new SelectExistPointCommand(this.reonMap))
: new PointTools(new PointToolsFactory());
}
}
In Angular, I have interfaces which look like this (this is not my code):
export interface Vehicles {
id: number;
cars: Car;
trucks: Truck;
}
Export interface Car {
make: number;
model: number;
}
Export interface Truck {
make: number;
model: number;
}
My service contains two functions doCarMath() and doTruckMath() that do calculations based on the API's returned values:
vehicleNumbers: Vehicles;
constructor(private http: HttpClient) {
this.vehicleNumbers = {};
}
callID(id: string): Observable<Vehicles>{
return this.http.get<Vehicles>(this.myURL+`/${id}`);
}
getMyCarNumbers(id) {
this.callID(id).subscribe(results =>
{
this.vehicleNumbers = results;
}
}
doCarMath(): number {
return this.vehicleNumbers.car.make; / this.vehicleNumbers.car.model;
}
doTruckMath(): number {
return this.vehicleNumbers.truck.make; / this.vehicleNumbers.truck.model;
}
I’d like to combine doCarMath() and doTruckMath() into one doMath() function by passing in an argument to specify either car or truck, like so:
doMath(p_vehicle): number {
return this.vehicleNumbers.p_vehicle.make/this.vehicleNumbers.p_vehicle.model; // can p_vehicle be used here?
}
How can I use parameter p_vehicle here? The above throws intellisense errors for the p_vehicle parameter, but I don't have the code locally and can't share the error.
I think this would do the job:
doMath(p_vehicle): number {
return this.vehicleNumbers[p_vehicle].make/this.vehicleNumbers[p_vehicle].model;
}
Then calling the function would be :
doMath('car'); // or this.doMath('car');
doMath('truck'); // or this.doMath('truck');
I'm pretty new with these languages and I'm sure it's not so complicated but could not find how to walk through class instance variables dynamically.
I'm trying to display instance's variables in a table using Angular. I'd like to write a single function that will work with any class.
Let's say I have a workbook class:
export class workbookEntity {
public name: string;
public creationDate: string;
constructor() {
this.name = 'my work book name';
this.creationDate = '2018/01/26';
}
}
And let's say I want to get the names and values of the variables of an instance of this class in another class' function:
export class showClassComponent {
// some code here
whenSubmitedInHtmlForm(className: string): void {
// do something to walk through instance
// with className parameter = 'workbookEntity'
}
// more code there
}
How would you walk through the instance to get each variable's name and value to get something like this?
[
{
name: 'name',
value: 'my work book name'
},
{
name: 'creationDate',
value: '2018/01/26'
}
]
There's no concept of reflection, so much in Typescript, so you can't neccessarily do a type lookup. But you might be able to do something along the lines of...
export class showClassComponent {
var classes = [ { name: 'workBookEntity', value: new WorkBookEntity() }]
whenSubmitedInHtmlForm(className: string): void {
let c = classes.filter(class => class.name == className)[0];
if(c) {
let ret = [];
for (var key in c) {
if (c.hasOwnProperty(key)) {
ret.push({ name: key, value: c[key] });
}
}
return ret;
}
}
// more code there
}
If you want to be able to retain accurate type information, you can create a pick utility function and then just import it into your class module wherever you need to use it.
let workBook = new WorkbookEntity();
export function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
let newObj = {} as Pick<T, K>;
for (const key of keys) {
newObj[key] = obj[key];
}
return newObj;
}
console.log(pick(workBook, ['name', 'creationDate']));
// log: { name: 'my work book name', creationDate: '2018/01/26' }
Then you can import the first function into another utility function if you to be able to handle multiple objects.
export function pickObjects<T extends object, K extends keyof T>
(objects: T[], keys: K[]): Pick<T, K>[] {
return objects.reduce((result, obj) => [...result, pick(obj, keys)], []);
}
let workbooks = pickObjects([workBook, workBook2], ['name']);
workbooks[0].name // this exists
workbooks[0].age // Typescript error.
The editor will show that age doesn't exist since we didn't pick it
So basically what I want to achieve is watching/listening objects changing inside of array within an injectable service using setters and getters to manipulate it's data
eg
#Injectable()
export class StorageService {
protected items: Array<any> = [];
constructor(private storage: Storage) {
this.storage.ready().then(() => {
StorageService.getGetters().forEach((get) => {
this.storage.get(get).then(res => this.items[get] = res);
});
});
}
public static getGetters(): string[] {
return Object.keys(this.prototype).filter(name => {
return typeof Object.getOwnPropertyDescriptor(this.prototype, name)["get"] === "function"
});
}
get Storage() {
return this.storage;
};
ClearStorage() {
this.storage.clear();
}
protected Setter(key: string, value: any): void {
this.items[key] = value;
this.storage.set(key, value);
}
protected Getter(key: string): any {
return this.items[key];
}
set User(value: User) {
this.Setter('User', value);
}
get User(): User {
return this.Getter('User');
}
}
where User interface is :
export interface User {
id: number;
role_id: number;
name: string;
email?: string;
}
now in any component or service/provider I can DI my StorageService so I can access the User getter.
so:
storage.User.name = 'testing';
now the name is changed , but I have no way to track that , so I can update my storage!
to update my storage I would do:
storage.User.name = 'testing';
storage.User = storage.User;
which is working , but I need a way to listen to any changes happens to the object properties, so I can update my storage...
I searched alot , and all I can find is watching components #Input() , which is not working in my case.
Hopefully I made my point clear.