I have a circular reference issue using this pattern approach. TypeError: Class extends value undefined is not a constructor or null .
The strange thing is, if I move the field.type.ts in src/constants.ts, it doesn't throw an error and it works as expected, but crashes on the Unit Tests. If it leave the fied.type.ts contents in it's own file, it crashes.
Maybe I am not using/understanding this dependency inversion pattern the right way. I could probably fixed by passing the FieldTypeToClassMapping as a parameter in Field.create(options: FieldOptions, fieldTypeMapping: FieldTypeToClassMapping), but I want to understand why this is happening.
import { StringField } from './string.field.model';
import { IntegerField } from './integer.field.model';
...
export const FieldTypeToClassMapping = {
//Constructor of eg. StringField class so I can use `new FieldTypeToClassMapping[options.type](options)`;
[FieldTypeEnum.STRING]: StringField,
[FieldTypeEnum.INTEGER]: IntegerField,
};
//field/field.ts
import { FieldOptions } from 'src/interfaces/field.options.interface';
import { FieldTypeToClassMapping } from 'src/model/template/field.type.to.mapping.ts'
export abstract class Field {
value: any;
type: string;
errors: string[] = [];
public constructor(options: FieldOptions) {
this.value = options.value;
this.type = options.type;
}
public static create(options: FieldOptions): any {
try {
return new FieldTypeToClassMapping[options.type](options);
} catch (e) {
throw new Error(`Invalid field type: ${options.type}`);
}
}
}
//field/integer.field.ts
import { FieldOptions } from 'src/interfaces/field.options.interface';
import { Field } from './field.model';
export class IntegerField extends Field {
constructor(options: FieldOptions) {
super(options);
}
protected validateValueDataType() {
this.validateDataType(this.value, "value");
}
protected validateDefaultDataType() {
this.validateDataType(this.defaultValue, "defaultValue");
}
}
//field/service.ts
payload const postFields = [
{
type: "string", //FieldTypeEnum.STRING,
value: 'a name'
},
];
const postFields = [
{
type: "string",
value: "John",
},
{
type: "integer",
value: 32,
},
];
const fieldsArray = [];
postFields.forEach((item) => {
const field: Field = Field.create(item);
fieldsArray.addField(field);
});
return fieldsArray;
The create(options: FieldOptions) function is defined inside the class Field, but then it tries to instantiate an instance of a class that extends Field.
I think that is where the problem arises. I don't know the entire contents of your files, but I imagine that at the top of any field.type.ts file you import Field. However since Field can instantiate any concrete implementation of itself it would need to know about them so you would need to import everything that extends Field inside Field.
I don't know/understand the dependency inversion pattern well enough to relate it to your question. But given the provided information, perhaps a Factory Pattern is what you need?
You could move the the function create(options: FieldOptions) to a FieldFactory class. Your create function is practically a factory function already.
Related
I am working on a Node Js (TypeScript) architecture and for some reason, I want to bind my interface to a specific object. I am making a general class that is extended by other subclasses and it will have a very general code. So my code looks like
interface User {
name: string;
}
interface Profile {
title: string;
}
class Parent {
name: string;
interface: Interface; // Help required here, getting error can't use type as a variable
constructor( name, interface ) {
// Load schema and store here
this.name = name
this.interface = interface
}
// Though this is not correct I hope you get the idea of what I am trying to do
get (): this.interface {
// fetch the data and return
return data
}
set (data: this.interface): void {
// adding new data
}
}
class UserSchema extends Parent {
// Class with custom functions for UserSchema
}
class ProfileSchema extends Parent {
// Class with custom functions for ProfileSchema
}
// Config file that saves the configs for different modules
const moduleConfig = [
{
name: "User Module",
class: UserSchema,
interface: User
},
{
name: "Profile Module",
class: ProfileSchema,
interface: Profile
},
]
const allModules = {}
// Loading the modules
moduleConfig.map(config => {
allModules[config.name] = new config.class(
config.name,
config.interface
)
})
export allModules;
I need suggestions on how should I bind my interfaces with their respective configs. Till now I have had no luck with that.
PS: All this code is separated into their respective files.
This is the use case for generics. You can even see them as "variable for types".
Instead of having an interface property in your Parent class, the latter would have a generic type:
class Parent<T> { // T is the generic type
name: string;
// interface: Interface; // generic is already provided at class level
constructor( name ) {
// Load schema and store here
this.name = name
}
get (): T {
// fetch the data and return
return data
}
set (data: T): void {
// adding new data
}
}
// Here you specify the concrete generic type
class UserSchema extends Parent<User> {
// Class with custom functions for UserSchema
}
class ProfileSchema extends Parent<Profile> {
// Class with custom functions for ProfileSchema
}
I am currently looking at learning Typescript Decorators. My first goal is to somewhat reproduce what #Slf4J from the Project Lombok does in Java to Typescript. The ideas is to annotate/decorate a class with e.g. #logger to receive a field log of type LogUtil within that same class in order to call e.g. log.info().
LogUtil class:
export class LoggerUtil {
logLevel: LogLevel;
constructor(logLevel: LogLevel) {
this.logLevel = logLevel;
}
error(className: string, message: string) {
if (this.logLevel >= LogLevel.ERROR) {
console.error(`${new Date()} [ERROR] ${className}: ${message}`);
}
}
warn(className: string, message: string) {
if (this.logLevel >= LogLevel.WARN) {
console.log(`${new Date()} [WARN] ${className}: ${message}`);
}
}
log(className: string, message: string): void {
console.log(`${new Date()} [LOG] ${className} ${message}`)
}
info(className: string, message: string): void {
if (this.logLevel >= LogLevel.INFO) {
console.log(`${new Date()} [INFO] ${className}: ${message}`)
}
}
call(className: string, message: string) {
if (this.logLevel >= LogLevel.INFO) {
console.log(`${new Date()} [CALL] ${className}.${message}`)
}
}
debug(className: string, message: string) {
if (this.logLevel >= LogLevel.DEBUG) {
console.log(`${new Date()} [DEBUG] ${className}: ${message}`)
}
}
}
LogLevel enum:
export enum LogLevel {
ERROR = 0,
WARN = 1,
INFO = 2,
DEBUG = 3
}
Example class using the #logger decorator to get an instance of LoggerUtil as log
#logger
export class SomeService {
exampleFunction() {
log.info("exampleFunction called")
}
}
I am currently trying to do this with the class-level decorators. Here I am trying to do different things:
Using the Reflect API to define a property on the class. Here I am not even sure if that even works.
export function logger() {
return function(target: Function) {
Reflect.defineProperty(target, "log", { value: new LoggerUtil(LogLevel.DEBUG) } )
}
}
Using the class prototype to define a property:
export function logger() {
return function(target: Function) {
target.prototype.log = new LoggerUtil(LogLevel.DEBUG);
}
}
With every approach I am getting "Cannot find name 'log'" when referencing the log instance within the Service:
#logger
export class SomeService {
exampleFunction() {
log.info("exampleFunction called") // Cannot find name 'log'
}
}
Is my idea possible at all? Is there something fundamental that I am missing?
Thank you vey much in advance for any feedback!
I found an approach that satisfies my needs at least a bit:
I went with inheritance (which I'd like to avoid). See below example:
#logger
export class SomeService extends Logger {
constructor() {
super()
}
exampleFunction() {
log.info("exampleFunction called")
}
}
Then the super class I am extending looks like this:
#logger
export class Logger {
log: LoggerUtil;
}
And finally, the decorator looks like this:
export function logger<T extends {new(...args:any[]):{}}>(constructor:T) {
return class extends constructor {
log = new LoggerUtil(LogLevel.DEBUG);
}
}
My idea was to add the field log to SomeService via inheritance. To initialize this field now, I used the decorator on the super class. The decorator itself returns a class that extends the decorator class with the initialized field. This way the following inheritance graph is created:
#logger (Decorator) -> Logger (super class) -> SomeService.
I could have initialized the log field within the super class Logger itself. However, this was to look into decorators and hopefully erase the super class in the long run.
As a reference I want to point to the Typescript documentation about how to overwrite the constructor.
I don't know if this is allowed in Typescript, but I'm working in an Angular 7 project and I want to instantiate a Page class fullfilling all his properties from DB object. These are my classes:
export class User {
id: number;
name: string;
created_at: string;
constructor(obj?: any) {
Object.assign(this, obj);
}
getName(): string {
return this.name;
}
}
export class Page {
id: number;
title: string;
author: User;
constructor(obj?: any) {
Object.assign(this, obj);
}
showTitle(): string {
return this.title;
}
}
Here is an example of my service method to retrieve the data:
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Page } from '../models/page';
#Injectable()
export class PageService {
constructor(httpClient: HttpClient) {}
getPage(id: number): Observable<Page> {
return this.httpClient
.get<Page>('http://<my-server-ip>:<port>/api/pages')
.pipe(
map((page: Page) => {
console.log('retrieved', page);
return new Page(page);
})
);
}
}
And here is an example of this function call in my component
export class MyCustomComponent implements OnInit {
constructor(pageService: PageService) {}
ngOnInit() {
this.pageService.getPage()
.subscribe((page: Page) => {
console.log(page.showTitle());
});
}
}
This example works, but when I want to access to User methods, like:
console.log(page.author.getName());
I don't have access to them because it is not an instantiation of User class.
The same would happen with Page if I do not return a new instance of page class as an observable, thats why I use return new Page(page) after retrieving the data.
The problem is that I want to keep my constructors as generic as possible, so creating a constructor to assign the value manually (e.g.: this.author = new User(obj.author);) is not a valid workaround, as I want to implement it in every model or create a GenericModel then extend all my models.
Is there a way to fill a property with defined type in a instantiated class depending in its type?
This is what I tried so far, but it doesn't work:
export class Page {
// ...
constructor(obj?: any) {
Object.keys(obj).forEach((key: string, index: number) => {
if (typeof(obj[key]) === 'object' && obj[key] !== null) {
this[key] = new (this[key].constructor)(obj[key]);
} else {
this[key] = obj[key]
}
});
}
}
ERROR TypeError: Cannot read property 'author' of null
ERROR TypeError: Cannot read property 'constructor' of undefined
I understand that this is null when constructor is called, but I couldn't find another way to fill author property with a new instance to access to methods. Also, if I get a standard/default object like { ... }, the if will trigger and probably will throw an error too, as it does not have a constructor.
You could use Object.assign like this:
getPage(id: number): Observable<Page> {
return this.httpClient
.get<Page>('http://<my-server-ip>:<port>/api/pages')
.pipe(
map((page: Page) => {
console.log('retrieved', page);
return Object.assign(new Page(), page);
})
);
}
This code creates a new Page instance and then copies over all of the properties from the returned response (page in this example).
Then you don't need to modify your constructors.
UPDATE
NOTE: The spread syntax only copies over the properties, so I changed to use Object.assign instead.
I'm having a strange problem with typescript interfaces. Because I'm using mongoose models I need to define one, but for some reason it's not recognising things that I have explicitly imported. This part works fine:
export interface ITrip extends mongoose.Document {
//
}
export var TripSchema = new mongoose.Schema({
//
});
export var Trip = mongoose.model<ITrip>('Trip', TripSchema);
Now, I'm defining another interface, that has an array of Trip. I need this for subdocuments.
import {Trip, ITrip} from '../trips/trip.model';
export interface IFeed extends mongoose.Document {
lastSnapshot: {
trips: [Trip]
}
}
The TS compiler gives this error: feed.ts(12,13): error TS2304: Cannot find name 'Trip'. (referring to trips: [Trip]). It doesn't say that the import failed or anything. I can even use trip inside the same file to create new objects var a = new Trip({}); without problem. Inside the interface it breaks.
Trip isn't a type, it's a variable, so you can do this:
let t = Trip;
let t2 = new Trip({});
But you can't do this:
let t: Trip;
You should change it to typeof Trip:
export interface IFeed extends mongoose.Document {
lastSnapshot: {
trips: [typeof Trip]
}
}
Also, if you want IFeed.lastSnapshot.trips to be an array, then it should be:
trips: typeof Trip[]
What you declared is a tuple of one item.
Edit
With an object the assignment is always the same (both js and ts):
let o = {
key: "value"
}
But when declaring types in typescript then you're not dealing with values:
interface A {
key: string;
}
let o: A = {
key: "value"
}
In the mongoose documentation they are using only javascript so all of their examples don't include the type declarations.
I've created a model in TypeScript that I'm using in a cast. When running the application, the model is not loaded and I'm unable to use any functions on that model.
Model
export class DataIDElement extends HTMLElement {
get dataID(): number {
var attributes: NamedNodeMap = this.attributes;
var dataIDAttribute: Attr = attributes.getNamedItem("data-id");
if (!dataIDAttribute) {
//throw error
}
var value: number = Number(dataIDAttribute.value);
return value;
}
}
Angular Component (Where model is being imported)
import { DataIDElement } from '../../models/dataIdElement';
export class PersonComponent
{
personClicked(event: KeyboardEvent): void {
var element: DataIDElement = <DataIDElement>event.target;
// This code always returns undefined (model isn't loaded)
var personID: number = element.dataID;
}
}
What you are doing there is a type assertion. That only overwrites the type inference of the compiler to make it believe that event.target is of the type DataIDElement. It doesn't create a new instance of DataIDElement.
If you want to create an instance of DataIDElement you need to create it using new.
DataIDElement would look something like this:
export class DataIDElement extends HTMLElement {
constructor(private target: HTMLElement) {}
get dataID(): number {
var attributes: NamedNodeMap = this.target.attributes;
var dataIDAttribute: Attr = attributes.getNamedItem("data-id");
if (!dataIDAttribute) {
//throw error
}
var value: number = Number(dataIDAttribute.value);
return value;
}
}
And would be used like this:
import { DataIDElement } from '../../models/dataIdElement';
export class PersonComponent
{
personClicked(event: KeyboardEvent): void {
var element: DataIDElement = new DataIDElement(event.target);
// This code always returns undefined (model isn't loaded)
var personID: number = element.dataID;
}
}