2

I am trying to define my data structure to encapsulate the logic for working with data in one class. I then have the Field class that has this information.

Field<T>(private name:string, private size:number, private comment:string) {

}

get Name():string {
    return this.name;
}

get Size():number {
    return this.size;
}

Create a Field reference:

class User {
    public FirstName:Field<string> = new Field<string>('FirstName', 20);
    public LastName:Field<string> = new Field<string>('LastName', 32);
    public Age:Field<number> = new Field<number>('Age', 3);
}

The above means I have a data field 'FirstName' which is a string and maximum of 20 characters. 'LastName' is a 32 character string and Age can be 3 digits.

I use this to then be able to validate my data on a form (using the Size for instance to limit the number of characters that could be entered) as well as when reading from an external API.

If I am getting the data from an external API, I can also use the field 'Size' to limit the amount of data I copy into the field, so as to truncate the data.

This allows my data interface layer to work with a data type class, expecting all fields to be Field<T>, and then allows me to use my library functions to control the data size, add data validation to forms etc., without always having to write the validate functions in the HTML, since I can use loops in Angular and extract the information from the data structure.

My question now is how to get a generic interface for working with data coming from lists and objects in AngularFire.

Normally, when accessing data and the structure from AngularFire, I can use:

constructor(public afDb:AngularFireDatabase) {
    ...
    this.afDb.object<IUser>('/user/1').valueChanges();
    ...
}

This would get the data and automatically parse it into the IUser interface (not shown above).

I want to be able to generate an IUser interface from my User class which has the data structures in the Field<T>. Basically I want to generate an interface from the User class something like:

export interface IUser {
    FirstName?:string;
    LastName?:string;
    Age?:number;
}

Then this interface could be used in my access to AngularFire. The other option/question would be how to do the equivalent of the afDb.object<IUser>... and leave the <IUser> off, but be able to parse the results from the AngularFire object into my data structure which is the User class. So the parsing would call Field<T>.SetValue(); or something.

1 Answer 1

1

If you want to build a totally generic interface to handle all different objects with multiple Field<T>s then you should probably be looking at creating something like this:

class Field<T> {

    constructor(private name: string, private size: number) {

    }
}

interface IField {
    [index: string]: Field<string | number>;
}

interface IWhatever {
    fields: ArrayLike<IField>;
}

That way you can use the IWhatever with afDb.object<IWhatever>() and get back an IWhatever that has an Array of objects with type IField, which is an interface that is very generic and can hold any amount of named properties, each of wich with a value of the concrete types Field<string> or Field<number> (you can expand on those types too as you require).

Is this what you are looking for?

-- UPDATE with more helpful guidance --

After reading your comments and reviewing again your answer I think that I now understand better what you might need. Take a look at the next example, which I think is more suitable to what you want to do. I commented each class and interface to make it clearer to understand the whole concept. Let me know if you need more clarification and if it helped.


// This is a implementation class of our concept of "Field" objects.
// The T is a generic type, which means that it can be any type every time
// you instantiate a new object of this class.
// the type will be used to define the type of the `value` variable 
// that will hold the actual internal value of the "Field".
//
// So when you create a new Field<string>(....) you will always know
// that the `value` property will be of type `string` and not anything else.
class Field<T> {

    public title: string;
    public maxLength: number;
    public value: T;

    constructor(title: string, maxLength: number) {
        this.title = title;
        this.maxLength = maxLength;
    }
}

// this is an interface, that defines an object with any number 
// of properties of type Field<string> or Field<number>
interface IObject {
    // can have any number of Field typed properties
    [index: string]: Field<string | number>;
}


// this is a more specific version of the above interface, that
// actually defines exactly which fields and  their types it 
// should have and which ones should be required or optional
interface IUser {

    // required fields
    firstName: Field<string>;
    lastName: Field<string>;
    age: Field<number>;

    // lets define an optional one
    favoriteColor?: Field<string>;
}

// Suppose that we get a data structure from somewhere that has no type 
// and that we want to encapsulate it inside one of our interfaces 
// in order to make it easier to work with it
//
// So lets create a literal object with no specific type (a data structure):
let data = {
    firstName: new Field<string>('First Name', 20),
    lastName: new Field<string>('Last Name', 32),
    age: new Field<number>('Age', 3),
}

// we can then assign this object or data structure to a 
// variable with the very generic IObject as type 
let anObjectOfUnknownType: IObject = data;

// no problem, no complaints from typescript, the data is compatible
// with our IObject interface!



// We can also assign it to a variable
// of type IUser, which is more specific
// and narrows down our data structure to the definition that
// we the IUser interface expects it to be. For this to work
// though we must make sure that the structure must satisfy 
// and be compatible with the IUser interface.
let userDataStructure: IUser = data;

// and yes, it works, because the data is compatible with IUser

// We now have encapsulated the "unknown" data structure in a 
// useful typed variable from which we can access its properties 
// and enjoy type checking and intellisense
console.log("user's full name is: " + userDataStructure.firstName.value + " " + userDataStructure.lastName.value);
console.log("user's age is: " + userDataStructure.age);

if (typeof userDataStructure.favoriteColor !== "undefined") {
    console.log("user's favorite color is: " + userDataStructure.favoriteColor.value);
}
Sign up to request clarification or add additional context in comments.

7 Comments

I think this just might be what I was after. Let me work it into my code to confirm.
@j-koutsoumpas I am going to have various IWhatever interfaces (IE: users, posts) and have the classes defined (User in the example) with the data structure. I believe your sample is simply then saying I create a single interface (IUsers) for each class, since the class has the fields Field<> defined. This gives a consistent interface for AngularFire and other code. Therefore I believe I would need an interface IUser { fields: ArrayLike<IField>; } and an interface IPosts { fields: ArrayLike<IField> }; Correct?
Yes that's correct. The IWhatever is the most generic one, a "catch-all" if you please, that can hold "any kind" of object that has 0 or more of properties that are compatible with the IField interface. Of course you should create more specific interfaces for those that you know about, like you said IUser, IPosts etc
Thanks. It worked as specified. Now my problem is how to make some optional in the interface, since some data (user Id, name) will always be set, but maybe a screen name might not be. Can I get away with fields?:ArrayLike<IField>
I updated my answer and added a more thorough example, check it out and let me know.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.