I am currently working on a Vue3 project where we use classes.
Since axios does not return class instances, we need to transform them and sometimes we even need to transform some properties.
Now I am working on an ActivityLog where I have a function convertChildrenToClasses:
export class ActivityLog extends BaseModel {
user?: any = undefined
activity = ''
activityResource = ''
oldValues: Record<string, string> = {}
newValues: Record<string, string> = {}
createdAt = ''
constructor() {
super();
}
get createdAtStr() {
if (this.createdAt) {
return moment(this.createdAt).format(dateTimeFormatString);
}
return null;
}
convertChildrenToClasses = () => {
console.log(this.user); // returns undefined, although previously filled
if (this.user) {
this.user = convertToClass<User>(new User(), {...this.user});
}
}
}
As you can see, we also have a method that recursively transforms the classes and their children, which is called before the previous one:
export function convertToClass<T>(emptyObject: T, entity: any): T {
const entityClass = Object.assign(emptyObject, entity);
if (entityClass.convertChildrenToClasses) {
if (entityClass instanceof ActivityLog) {
console.log(entityClass.user); // Returns an object with values and is not manipulated anywhere else
}
entityClass.convertChildrenToClasses() // console.log inside the class method returns undefined
}
return entityClass as T;
}
Now iam wondering, why the console.log() in convertToClass returns an object with values:
{#id: '/api/users/7', #type: 'User', id: 7, email: 'artur.smolen#explicatis.com', firstName: 'Artur', …}
and the console.log() in convertChildrenToClasses returns undefined
This is the method, where all starts:
function pushToCollection(entities: T[]) {
collection.value.push(...entities.map(entity => convertToClass(Object.create(initialValue.value), entity)))
}
In the current case initialValue has new ActivityLog() as value and the entities are the server-reponse
Related
I have a class with a lot of parameters, a simplified version is shown below:
class data {
ID: string;
desp: string;
constructor(con_ID:string,con_desp:string){
this.ID = con_ID;
this.desp = con_desp;
}
}
I am then receiving data from a RESTful call, the body of the call is JSON. It might not have all the parameters requried to create an instance of data. Below is an example of the desp not being passed.
const a = JSON.stringify({ ID: 'bob' });
const b = JSON.parse(a)
If I try to create a new instance of data, it works.
console.log(new data(b['ID'], b['desp']))
>> data { ID: undefined, desp: 'bob' }
How do I reject the construction of the class if a parameter from JSON is undefined?
One method would be to do this for each parameter within the constructor, but I don't think this is the correct solution:
if (con_ID== undefined){
throw new Error('con_ID is undefined')
}
We can utilize class decorators for this. If we return a class from the decorator then the class' constructor will replace the one defined in code. Then we use parameter decorators to store the index of each parameter we wish to check into an array.
const noUndefinedKey = Symbol("noUndefinedKey");
const NoUndefined: ParameterDecorator = function (target, key, index) {
const data = Reflect.getMetadata(noUndefinedKey, target) ?? [];
data.push(index);
Reflect.defineMetadata(noUndefinedKey, data, target);
};
const Validate = function (target: { new (...args: any[]): any }) {
const data = Reflect.getMetadata(noUndefinedKey, target);
return class extends target {
constructor(...args: any[]) {
data.forEach((index: number) => {
if (typeof args[index] === "undefined") throw new TypeError(`Cannot be undefined.`);
});
super(...args);
}
}
}
Note that reflect-metadata must be used to use Reflect.getMetadata and Reflect.defineMetadata. Here's how you would use it:
#Validate
class ProtectedFromUndefined {
constructor(#NoUndefined param: string) {}
}
And try a few things:
//#ts-ignore throws error because undefined was provided
new ProtectedFromUndefined()
//#ts-ignore
new ProtectedFromUndefined(undefined)
// is ok
new ProtectedFromUndefined("")
Playground
I've some code like:
const methodsList = [
'foo',
'bar',
// ... 20 other items ...
]
export class Relayer {
constructor() {
for (const methodName of methodsList) {
this[methodName] = (...args) => {
// console.log('relaying call to', methodName, args)
// this is same for all methods
}
}
}
}
const relayer = new Relayer()
relayer.foo('asd') // TS error
relayer.bar('jkl', 123) // TS error
Now when I use the class instance, TypeScript complains when I call relayer.foo() or relayer.bar(). To make the code compile, I've to cast it as any or similar.
I've an interface that declares foo, bar and the other methods:
interface MyInterface {
foo: (a: string) => Promise<string>
bar: (b: string, c: number) => Promise<string>
// ... 20 other methods
}
How do I get TypeScript to learn the dynamically declared foo and bar class methods? Can the declare syntax be useful here?
First step is to create a type or interface where when indexed by a value in methodsList, the result will be a function:
// The cast to const changes the type from `string[]` to
// `['foo', 'bar']` (An array of literal string types)
const methodsList = [
'foo',
'bar'
] as const
type HasMethods = { [k in typeof methodsList[number]]: (...args: any[]) => any }
// Or
type MethodNames = typeof methodsList[number] // "foo" | "bar"
// k is either "foo" or "bar", and obj[k] is any function
type HasMethods = { [k in MethodNames]: (...args: any[]) => any }
Then, in the constructor, to be able to assign the keys of methodsList, you can add a type assertion that this is HasMethods:
// General purpose assert function
// If before this, value had type `U`,
// afterwards the type will be `U & T`
declare function assertIs<T>(value: unknown): asserts value is T
class Relayer {
constructor() {
assertIs<HasMethods>(this)
for (const methodName of methodsList) {
// `methodName` has type `"foo" | "bar"`, since
// it's the value of an array with literal type,
// so can index `this` in a type-safe way
this[methodName] = (...args) => {
// ...
}
}
}
}
Now after constructing, you have to cast the type still:
const relayer = new Relayer() as Relayer & HasMethods
relayer.foo('asd')
relayer.bar('jkl', 123)
You can also get rid of the casts when constructed using a factory function:
export class Relayer {
constructor() {
// As above
}
static construct(): Relayer & HasMethods {
return new Relayer() as Relayer & HasMethods
}
}
const relayer = Relayer.construct()
Another way around it is to create a new class and type-assert that new results in a HasMethods object:
class _Relayer {
constructor() {
assertIs<HasMethods>(this)
for (const methodName of methodsList) {
this[methodName] = (...args) => {
// ...
}
}
}
}
export const Relayer = _Relayer as _Relayer & { new (): _Relayer & HasMethods }
const relayer = new Relayer();
relayer.foo('asd')
relayer.bar('jkl', 123)
Or if you are only using new and then methods in methodsList, you can do:
export const Relayer = class Relayer {
constructor() {
assertIs<HasMethods>(this)
for (const methodName of methodsList) {
this[methodName] = (...args) => {
// ...
}
}
}
} as { new (): HasMethods };
You can also use your MyInterface interface instead of HasMethods, skipping the first step. This also gives type-safety in your calls.
Use the following syntax:
export class Relayer {
constructor() {}
public foo(){
// your foo method
this.executedOnEachFunction();
}
public bar(){
// your bar method
this.executedOnEachFunction();
}
executedOnEachFunction(){
// what you want to do everytime
}
}
https://repl.it/repls/LawfulSurprisedMineral
To me, this sounds like a need for an interface.
interface MyInterface {
foo(): void; // or whatever signature/return type you need
bar(): void;
// ... 20 other items ...
}
export class Relayer implements MyInterface {
constructor() {}
foo(): void {
// whatever you want foo to do
}
// ... the rest of your interface implementation
}
What it looks like you are doing is implementing some interface of sorts. In your constructor you are defining what the method implementations are instead of defining them in the class body. Might help to read Class Type Interfaces
class test{
name : string
children : Map<string,string> =new Map()
constructor(){
this.name='KIANA'
this.children.set('name','OTTO')
}
}
var t = new test()
console.log(t)
console.log(JSON.stringify(t))
The result is:
test { children: Map { 'name' => 'OTTO' }, name: 'KIANA' }
{"children":{},"name":"KIANA"}
How can I get the whole data like:
{"children":{'name':'OTTO'},"name":"KIANA"}
or
{"children":['name':'OTTO'],"name":"KIANA"}
Or, does it has a simpler way to describe the relationship of 'key value' in JSON and TypeScript
Preface: Class names should start with an uppercase character, so I've changed test to Test in the below.
Since Map isn't stringify-able by default, you have at least three choices:
Implement toJSON on your Test class and return an object with a replacement for children (probably an array of arrays), or
Implement a subclass of Map that has toJSON and use that in Test
Implement a replacer that you use with JSON.stringify that handles Map instances.
While #1 works, it means you have to edit your toJSON method every time you add or remove properties from Test, which seems like a maintenance issue:
class Test {
name: string
children: Map<string, string> = new Map()
constructor() {
this.name = 'KIANA'
this.children.set('name', 'OTTO')
}
toJSON() {
return {
name: this.name,
children: [...this.children.entries()]
}
}
}
var t = new Test()
console.log(JSON.stringify(t))
Live Example:
class Test {
name/*: string*/
children/*: Map<string, string>*/ = new Map()
constructor() {
this.name = 'KIANA'
this.children.set('name', 'OTTO')
}
toJSON() {
return {
name: this.name,
children: [...this.children.entries()]
}
}
}
var t = new Test()
console.log(JSON.stringify(t))
[...this.children.entries()] creates an array of [name, value] arrays for the map.
But I prefer #2, a JSON-compatible Map:
class JSONAbleMap extends Map {
toJSON() {
return [...this.entries()]
}
}
...which you then use in Test:
class Test {
name: string
children: Map<string, string> = new JSONAbleMap()
constructor() {
this.name = 'KIANA'
this.children.set('name', 'OTTO')
}
}
var t = new Test()
console.log(JSON.stringify(t))
Live Example:
class JSONAbleMap extends Map {
toJSON() {
return [...this.entries()]
}
}
class Test {
name/*: string*/
children/*: Map<string, string>*/ = new JSONAbleMap()
constructor() {
this.name = 'KIANA'
this.children.set('name', 'OTTO')
}
}
var t = new Test()
console.log(JSON.stringify(t))
Or #3, a replacer function you use with JSON.stringify:
function mapAwareReplacer(key: string|Symbol, value: any): any {
if (value instanceof Map && typeof value.toJSON !== "function") {
return [...value.entries()]
}
return value
}
...which you use when calling JSON.stringify:
console.log(JSON.stringify(t, mapAwareReplacer))
Live Example:
function mapAwareReplacer(key, value) {
if (value instanceof Map && typeof value.toJSON !== "function") {
return [...value.entries()]
}
return value
}
class Test {
name/*: string*/
children/*: Map<string, string>*/ = new Map()
constructor() {
this.name = 'KIANA'
this.children.set('name', 'OTTO')
}
}
var t = new Test()
console.log(JSON.stringify(t, mapAwareReplacer))
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
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.