Cannot access object properties from dynamic import. Vue.js + ts - javascript

My customer wants to have other instances of the app with another content as well (so called 'themes'). We decided to use env variable + prefilled object with content. So, this content may exist or it may not.
I created a function that conditionally imports module with all the content and it actually even working:
theme: string;
hasAnyTheme: boolean;
static getThemedMiscContent(): {[key: string]: string} {
let miscContent = {};
if (hasAnyTheme) {
import(`#/themes/customers-themes/${theme}/ThemedContent`)
.then(themedContent => {
miscContent = Object.assign(miscContent, themedContent.ThemedMiscContent);
});
}
return miscContent;
}
But when I call it from the component, I can't actually read properties of the object while I can read the object itself.
// computed
miscThemedContent(): {[key: string]: string} {
return ThemeUtil.getThemedMiscContent();
}
// template
{{ miscThemedContent }} // is totally fine
{{ miscThemedContent.someValue }} // undefined
And as the weird fact, this issue appears in only one of my 3 components that use that function. Others work just fine.
As far, as I understood, that kind of error appears when Vue tries to use that value before the object is loaded. So, I tried to add additional loading variable and nothing is happening. Is there any way to fix that?

Since import is an async function, miscContent could be returned before the import function's execution is completed. I suggest you make use of async/await syntax to wait for the actual result before returning the miscContent, it should be something like this:
static async getThemedMiscContent(): {[key: string]: string} {
let miscContent = {};
if (hasAnyTheme) {
const importTheme = await import(`#/themes/customers-themes/${theme}/ThemedContent`);
if (importTheme && importTheme.ThemedMiscContent) {
miscContent = Object.assign(miscContent, importTheme.ThemedMiscContent);
}
}
return miscContent;
}

Related

How to make class instance constructable in TypeScript?

I'm trying to convert this package to TypeScript without any breaking changes. I have the following code in TypeScript.
// DocumentCarrier.ts
/* export */ class DocumentCarrier {
internalObject: {};
model: Model;
save: (this: DocumentCarrier) => void;
constructor(model: Model, object: {}) {
this.internalObject = object;
this.model = model;
}
}
DocumentCarrier.prototype.save = function(this: DocumentCarrier): void {
console.log(`Saved document ${JSON.stringify(this.model)} to ${this.model.myName}`);
};
// Model.ts
// import {DocumentCarrier} from "./DocumentCarrier.ts";
/* export */class Model {
myName: string;
Document: typeof DocumentCarrier;
get: (id: number) => void;
constructor(name: string) {
this.myName = name;
const self: Model = this;
class Document extends DocumentCarrier {
static Model: Model;
constructor(object: {}) {
super(self, object);
}
}
Document.Model = self;
Object.keys(Object.getPrototypeOf(this)).forEach((key) => {
Document[key] = this[key].bind(this);
});
this.Document = Document;
return this.Document as any;
}
}
Model.prototype.get = function(id: number): void {
console.log(`Retrieving item with id = ${id}`);
}
// Usage
// index.ts
// import {Model} from "./Model.ts";
const User = new Model("User");
const user = new User({"id": 5, "name": "Bob"});
user.save(); // "Saved document {"id": 5, "name": "Bob"} to User"
console.log(User.Model.myName); // "User"
// console.log(User.myName); // "User" // This option would be even better, but isn't supported in the existing code
User.get(5); // "Retrieving item with id = 5"
In the Usage section (very bottom of the code example above) I'm getting multiple errors in TypeScript. But running that code in a JavaScript file, works perfectly. So I know it's working and the code is accurate.
I think the biggest problem of what I'm trying to do is return this.Document as any. TypeScript is interpreting that as casting this.Document to a Model instance, when in reality it's not.
My question is this. In TypeScript how can I set it up where you can run new MyClassInstance() and have it return an instance of a different class? That has a bidirectional reference from MyClassInstance and the different class. In short, how do I get the following code working?
It's important that any solution works with the Usage section, and no modifications are made to that section. Except for the User.Model.myName vs User.myName section, which would be preferred as User.myName, but in the existing version functions as User.Model.myName.
For easy use, I also created a TypeScript Playground.
I'm going to interpret this question strictly as "how can I give typings to the existing code so that the compiler understands the code in the Usage section?" That is, the answer should not touch the emitted JavaScript, but instead should only alter type definitions, annotations, and assertions.
Aside: the more general question "how should I implement a class whose instances are themselves class constructors" is one I won't attempt to address, since from my research the best answer here is "don't try to do that" since it plays poorly with the prototypical inheritance model in JS. I'd instead lean strongly toward having a non-constructible class instance hold a property which is the constructor of the new class. Something like this Playground code. You'd be a lot happier in the long run, I expect.
Back to the typings: the main problem here is that TypeScript has no way to specify that a class constructor returns a type other than the class being defined. This is either intentional (see microsoft/TypeScript#11588 or a missing feature (see microsoft/TypeScript#27594) but in any case it's not part of the language.
What we can do here is to use declaration merging. When you write class Model {} you introduce both a class constructor object named Model and an interface type named Model. That interface can be merged into, adding methods and properties that the compiler doesn't already know about. In your case you could do this:
interface Model {
new(object: {}): DocumentCarrier;
Model: Model;
}
This lets the compiler know that Model instances, in addition to having the properties/methods declared in the class, also has a Model property whose type is Model, and, importantly, a constructor signature. That's enough to get the following code to compile without error:
const User = new Model("User");
const user = new User({ "id": 5, "name": "Bob" });
user.save(); // "Saved document {"id": 5, "name": "Bob"} to User"
console.log(User.Model.myName); // "User"
User.get(5); // "Retrieving item with id = 5"
The compiler does think that User.myName exists, which it doesn't at runtime, but that's already a problem with the existing code so I'm not touching that here. It's possible to change the typings further so that the compiler knows that User.Model.myName exists and that User.myName does not exist, but that becomes quite complicated as it requires you to split Model's interface into multiple types that you carefully assign to the right values. So for now I'm ignoring it.
The only other change I'd make here would be to give different typings to the implementation of Model, like this:
class Model {
myName: string;
Document: Model;
get!: (id: number) => void;
constructor(name: string) {
this.myName = name;
const self: Model = this;
class Document extends DocumentCarrier {
static Model: Model;
constructor(object: {}) {
super(self, object);
}
}
Document.Model = self;
(Object.keys(Object.getPrototypeOf(this)) as
Array<keyof typeof DocumentCarrier>).forEach((key) => {
Document[key] = this[key].bind(this);
});
this.Document = Document as Model;
return this.Document;
}
}
The only thing the compiler won't be able to verify in the above is that the Document class is a valid Model, so we use the assertion Document as Model. Other than that I just put a few assertions (get is definitely assigned, and Object.keys() will return an array of keys of the DocumentCarrier constructor) so that you don't need to turn off the --strict compiler flag.
Okay, hope that helps. Good luck!
Playground link to code
After roaming a bit, I got something.
Typescript complains about your solution because, even if you are returning a class Document internally in Model constructor, the compiler expects a Model instance, which is not constructable.
So, we need to make Model constructable. In fact, the same as making a function which returns instances of something.
First, let's declare your preovious DocumentCarrier class. Now, DocumentCarrier will have two properties, model and name (this was your previously keyed myName from Model class).
class DocumentCarrier {
name: string = ``;
constructor(public model: {}) { }
save = () => console.log(`Saved document ${JSON.stringify(this.model)} to ${this.name}`)
}
After that, we need that function declaration that returns an instance model of type DocumentCarrier.
const Model = (name: string) => {
return class extends DocumentCarrier {
name: string = name;
constructor(model: any) {
super(model);
}
}
}
The function takes a string parameter and returns a constructor of type DocumentCarrier which takes an any object on its constructor and passes to the DocumentCarrier constructor.
We can call Model like this.
const User = Model('User');
const user = new User({id: 5, name: 'Bob'});
user.save(); // Saved document {"id":5,"name":"Bob"} to User
The call to Model is the only change made. Now Model call does not need a new keyword.
On the other hand, name in DocumentCarrier can be accessed from the last instance constructed.
In addition, this solution could be a bit more generic by defining generic types.
type Constructor<T> = new (...args: any[]) => T;
The type Constructor constraints a constructor function of any type T.
Now, we need to rewrite the Model function:
const Model = <T extends Constructor<{}>>(Type: T, name: string) => {
return class extends Type {
name: string = name;
constructor(...args: any[]) {
super(...args);
}
}
}
Model now expects the same string as before but an additional parameter which is the type we want to instantiate, so
const User = Model(DocumentCarrier, 'User');
const user = new User({id: 5, name: 'Bob'});
user.save(); // Saved document {"id":5,"name":"Bob"} to User
Even more, since we are fulfilling a property name that belongs to the instance we are creating inside the Model function, we should constraint the input type to expect that name property.
type Constructor<T> = new (...args: any[]) => T;
interface DynamicallyConstructed {
name: string;
}
class DocumentCarrier implements DynamicallyConstructed {
name: string = ``;
constructor(public model: {}) { }
save = () => console.log(`Saved document ${JSON.stringify(this.model)} to ${this.name}`)
}
const Model = <T extends Constructor<DynamicallyConstructed>>(Type: T, name: string) => {
return class extends Type {
name: string = name;
constructor(...args: any[]) {
super(...args);
}
}
}
const User = Model(DocumentCarrier, 'User');
const user = new User({id: 5, name: 'Bob'});
user.save();
Hope this helps a bit.
Please comment any issue.

TypeError: <...> is not a function

So, I'm running into a problem and I'm not sure exactly how to resolve it. After reading through the ES6 doc's I think I have this set up correctly, yet when I call <UserInstance>.getID() I get the error:
TypeError: currentUser.getID is not a function.
I know this may be a duplicate but in the other questions I've seen answer similar questions, none of them have allowed me to resolve this issue.
Here's my class definition:
import { v4String } from "uuid/interfaces";
class User {
private id!: v4String;
constructor() {
this.getID = this.getID.bind(this);
this.setID = this.setID.bind(this);
}
getID = () => this.id;
setID = (id: v4String) => this.id = id;
}
export default User;
I'm pretty sure I have the class set up, but is there something I'm missing with the arrow function? It doesn't seem to matter if I set it up with the arrow function syntax, or set it up like
getID() {
return this.id
}
Here's the code that's calling it, currentUser is provided by a context provider and injected into the props using a Higher Order Component:
componentDidMount() {
const currentUser: User = this.props.currentUser;
this.setState({ loading: true });
const currentUserID = currentUser.getID(); <---- FAILS HERE
const currentUserIDString = currentUserID.toString();
}
}
TypeError: currentUser.getID is not a function.
This error means that currentUser is some value which does not have a getID method on it. Your class is fine, so something is wrong with the value of currentUser and not the User class.
It appears that currentUser is a plain javascript object, and not an instance of your class.
const currentUser: User = this.props.currentUser;
This line does not make currentUser an instance of User, it merely a type hint for typescript. And it is a type hint that is incorrect.
Somewhere (where is up to you) you need to call new User() in order to be able to use the methods that you have defined on your user class. If you never call new User() then you do not have an instance of User, you just have a plain object.

Vue.js directive v-html not updating if the model is overwritten

By running the following code (a Vue.js component), I expect that, after the AJAX call returns, both the v-html directive and the console.log() display the same value.
On the contrary, v-html is stuck with "loading...(1)" even though obj.html has a different value, as console.log() confirms.
The behaviour is caused by getObject overwriting obj, and being afterwards obj.html undefined for a short time before getHTML returns (all this happens in function created).
Can please someone explain whether this is Vue's desired behavior (doc links are welcome), or whether should I submit a bug report, or finally whether I am simply structuring my code in a bad way?
Thanks in advance
<template>
<main v-html="obj.html || 'loading... (1)'">
</main>
</template>
<script>
export default {
name: 'Post',
data: function () {
return {
obj: {
html: 'loading... (2)'
}
}
},
created: async function () {
this.obj = await this.getObject()
this.obj.html = await this.getHtml()
console.log(this.obj.html)
},
methods: {
getObject: async function () {
const resp = await this.$http.get('https://jsonplaceholder.typicode.com/todos')
return resp.body[0]
},
getHtml: async function () {
const resp = await this.$http.get('https://jsonplaceholder.typicode.com/todos')
return resp.body[0].title
},
}
}
</script>
The function getObject returns a String so at the first line of created hook
this.obj = await this.getObject()
you change the reference of the obj and you make it pointing to a string and then you try to put a property on a string, which does not work ;)
it's like you would do
this.obj = 'test'
then console.log(this.obj);
// test
and then this.obj.abc = 'whatever'
console.log(this.obj.abc);
// undefined
You would need to parse the object before, see JSON.parse(string)
Update:
If this is not the case i.e you somehow have an object coming from that service.
Then the only problem I can think is that you lose the reference of your original obj and v-html is still pointing to the old one. In that case you have to avoid modification of the root obj or you can use the vue $set method: https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
It seems vue data objects are not deeply reactive, which means that altering a property will not trigger change detection in the template.
Try rearranging the created hook to compose the full object before assigning it to the data property. That way when the template reacts it will see the html property of obj.
Ref CodeSandbox
created: async function () {
const fetchedObj = await this.getObject()
fetchedObj.html = await this.getHtml()
this.obj = fetchedObj;
console.log(this.obj.html)
},

TypeScript: auto-generated dynamic function names

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.

Overriding javascript Object toString() in a custom typescript class

I've been sitting with a problem for the past days & I can't seem to get a solution anywhere.
Background:
I have a typescript class defined as follows:
export class Client extends Person {
claimNumber: string;
policyNumber: string;
address: string;
insuranceCompany: Organisation = new Organisation();
toString(): string {
return this.policyNumber
.concat(this.claimNumber);
}
}
The above is used as a model that drives an angular 5 template. In my component, I fetch (using angular 5 HttpClient) a list of clients from a remote api & generate an html table rows. The LOC to generate the table rows is:
<tr *ngFor="let client of clients | filter:searchString"> ... </tr>
searchString above is property bound to a search input tag & filter is a custom filter Pipe defined as follows:
export class FilterPipe implements PipeTransform {
transform(items: Client[], term: string) {
if (term == undefined || term === '') return items;
return items.filter(item =>item.toString().toLocaleLowerCase().includes(term.toLocaleLowerCase()));
}
}
Problem:
When I inspect item.toString() in the filter pipe above, it returns [object Object] as opposed to a string made up of policyNumber, & claimNumber.
Investigation:
I investigated this issue as follows: I instantiated the Client class as follows:
let c = new Client();
c.policyNumber = 'ababababa';
c.claimNumber = 'aaaaaaa';
console.log('client toString() is => ' + c.toString());
Interesting enough, the console.log above outputs : 'ababababaaaaaaaa'.
Question:
What am I doing wrong that results in the item.toString() in the filter pipe return [object Object] whereas toString() on a class I instantiated returns the correct string?
If you get the clients from a WebService (or something similar), you are just getting plain json objects. If you say that the received objects are of type Client, typescript will show them as objects of such type, but only the properties will be the same, the methods will not be from the Client class, but from the Object class.
You might want to instantiate them as real client objects after you retrieve them from the server:
public myServiceMethod() {
return this.http.get(...).map(plainClients => {
const realClients: Array<Client> = (plainClients || []).map(plainClient => {
let realClient = new Client();
realClient.claimNumber = plainClient.claimNumber;
realClient.policyNumber = plainClient.policyNumber;
realClient.address = plainClient.address;
return realClient;
});
return realClients;
})
}
You might prefer to use anemic objects with their types being interfaces, and use an utilitarian function to retrieve the client as a string, to avoid cases like the one you're having:
export interface Person {
...
}
export interface Client extends Person {
claimNumber: string;
policyNumber: string;
address: string;
insuranceCompany: Organisation;
}
// In some utilitarian class
public static getClientInfo(client: Client) {
return client.policyNumber.concat(client.claimNumber);
}
// In your pipe
return items.filter(item => getClientInfo(item).toLocaleLowerCase().includes(term.toLocaleLowerCase()));
I'm not saying to just use anemic classes in your app, but if some type of object is passed around and will probably be serialized, then using anemic objects can avoid problems like the one you are having.
A way to figure out what the problem might be would be to rename your method to something that isn't a built in method name, like maybe toSearchString. It would also be worth adding console logs to your filter function to make sure you're actually getting clients there. You may actually be getting a different object.

Categories

Resources