Generic User Collection - ldco2016/microurb_web_framework GitHub Wiki

So I need to turn my Collection class into a generic class and essentially remove every reference to User from it:

import axios, { AxiosResponse } from "axios";
import { User, UserProps } from "./User";
import { Eventing } from "./Eventing";

export class Collection {
  models: User[] = [];
  events: Eventing = new Eventing();

  constructor(public rootUrl: string) {}

  get on() {
    return this.events.on;
  }

  get trigger() {
    return this.events.trigger;
  }

  fetch(): void {
    axios.get(this.rootUrl).then((response: AxiosResponse) => {
      response.data.forEach((value: UserProps) => {
        const user = User.buildUser(value);
        this.models.push(user);
      });

      this.trigger("change");
    });
  }
}

If we have any reference to User or UserPropos inside of here that means I will not be able to use the class of Collection for other types of data inside my application as well.

So after removing the import statement for the User model then I will classify Collection as a generic class like so:

import axios, { AxiosResponse } from "axios";

import { Eventing } from "./Eventing";

export class Collection<T> {
  models: User[] = [];
  events: Eventing = new Eventing();

  constructor(public rootUrl: string) {}

  get on() {
    return this.events.on;
  }

  get trigger() {
    return this.events.trigger;
  }

  fetch(): void {
    axios.get(this.rootUrl).then((response: AxiosResponse) => {
      response.data.forEach((value: UserProps) => {
        const user = User.buildUser(value);
        this.models.push(user);
      });

      this.trigger("change");
    });
  }
}

So T can represent the User class, but in some future it could also represent a blog post, a class image or comment or whatever else. T is essentially the type of whatever collection of things I have stored inside of here.

So I will say that models is now an element that contains a type of T like so:

import axios, { AxiosResponse } from "axios";

import { Eventing } from "./Eventing";

export class Collection<T> {
  models: T[] = [];
  events: Eventing = new Eventing();

  constructor(public rootUrl: string) {}

  get on() {
    return this.events.on;
  }

  get trigger() {
    return this.events.trigger;
  }

  fetch(): void {
    axios.get(this.rootUrl).then((response: AxiosResponse) => {
      response.data.forEach((value: UserProps) => {
        const user = User.buildUser(value);
        this.models.push(user);
      });

      this.trigger("change");
    });
  }
}

I now have a reference to UserProps I need to replace as well. This is a troubling note because right now type T is a User class and the User class doesn't have a reference to type UserProps. So I need to essentially add a second generic type to my class to allow me to specify the type of UserProps. To do so, I can just add on a second generic type at the top like so:

import axios, { AxiosResponse } from "axios";

import { Eventing } from "./Eventing";

export class Collection<T, K> {
  models: T[] = [];
  events: Eventing = new Eventing();

  constructor(public rootUrl: string) {}

  get on() {
    return this.events.on;
  }

  get trigger() {
    return this.events.trigger;
  }

  fetch(): void {
    axios.get(this.rootUrl).then((response: AxiosResponse) => {
      response.data.forEach((value: UserProps) => {
        const user = User.buildUser(value);
        this.models.push(user);
      });

      this.trigger("change");
    });
  }
}

With functions in general, we can pass in one, two or more arguments, same thing with generics.

So all I am doing is saying whenever I refer to Collection I am going to pass in a type for T and type for K as well.

In this case, K will be the type that specifies the structure of JSON data that I get back like so:

import axios, { AxiosResponse } from "axios";

import { Eventing } from "./Eventing";

export class Collection<T, K> {
  models: T[] = [];
  events: Eventing = new Eventing();

  constructor(public rootUrl: string) {}

  get on() {
    return this.events.on;
  }

  get trigger() {
    return this.events.trigger;
  }

  fetch(): void {
    axios.get(this.rootUrl).then((response: AxiosResponse) => {
      response.data.forEach((value: K) => {
        const user = User.buildUser(value);
        this.models.push(user);
      });

      this.trigger("change");
    });
  }
}

The next big issue is that I have a direct reference to a function that is going to attempt to create a user out of some JSON data. I cannot replace it with a generic because User is an actual class, an actual object, not a type and I am attempting to call an actual function.

T and K only goes in locations where I am attempting to specify a type.

To solve this, I need to pass in a function to be used to take some JSON data and return an instance of a model like so:

import axios, { AxiosResponse } from "axios";

import { Eventing } from "./Eventing";

export class Collection<T, K> {
  models: T[] = [];
  events: Eventing = new Eventing();

  constructor(public rootUrl: string, public deserialize:) {}

  get on() {
    return this.events.on;
  }

  get trigger() {
    return this.events.trigger;
  }

  fetch(): void {
    axios.get(this.rootUrl).then((response: AxiosResponse) => {
      response.data.forEach((value: K) => {
        const user = User.buildUser(value);
        this.models.push(user);
      });

      this.trigger("change");
    });
  }
}

The deserialize function is going to take in some JSON data and turn it into an actual instance of an object and I will pass it to my collection whenever I create an instance of it. This will allow me to reuse the collection for any type of data I want.

The type of deserialize is going to be a function that is going to take in some JSON and the JSON will be of type K because essentially K is the UserProps with the name, id and age. So this would look like so:

import axios, { AxiosResponse } from "axios";

import { Eventing } from "./Eventing";

export class Collection<T, K> {
  models: T[] = [];
  events: Eventing = new Eventing();

  constructor(public rootUrl: string, public deserialize: (json: K) =>) {}

  get on() {
    return this.events.on;
  }

  get trigger() {
    return this.events.trigger;
  }

  fetch(): void {
    axios.get(this.rootUrl).then((response: AxiosResponse) => {
      response.data.forEach((value: K) => {
        const user = User.buildUser(value);
        this.models.push(user);
      });

      this.trigger("change");
    });
  }
}

So its going to take an argument of type K and return something of type T, which is the actual thing I am storing inside the models array like so:

import axios, { AxiosResponse } from "axios";

import { Eventing } from "./Eventing";

export class Collection<T, K> {
  models: T[] = [];
  events: Eventing = new Eventing();

  constructor(public rootUrl: string, public deserialize: (json: K) => T) {}

  get on() {
    return this.events.on;
  }

  get trigger() {
    return this.events.trigger;
  }

  fetch(): void {
    axios.get(this.rootUrl).then((response: AxiosResponse) => {
      response.data.forEach((value: K) => {
        const user = User.buildUser(value);
        this.models.push(user);
      });

      this.trigger("change");
    });
  }
}

Now I need to ensure I call deserialize with each piece of json record and get back the instance of the model. To do so, I deleted the entire const user line and pushed in this.deserialize(value) like so:

import axios, { AxiosResponse } from "axios";

import { Eventing } from "./Eventing";

export class Collection<T, K> {
  models: T[] = [];
  events: Eventing = new Eventing();

  constructor(public rootUrl: string, public deserialize: (json: K) => T) {}

  get on() {
    return this.events.on;
  }

  get trigger() {
    return this.events.trigger;
  }

  fetch(): void {
    axios.get(this.rootUrl).then((response: AxiosResponse) => {
      response.data.forEach((value: K) => {
        this.models.push(this.deserialize(value));
      });

      this.trigger("change");
    });
  }
}

I am going to stick in the json data I get. I should get back an instance of a model that I am going to add to my array of models.

Now inside of my index.ts file, I have an error around my new Collection():

import { Collection } from "./models/Collection";

const collection = new Collection("http://localhost:3000/users");

collection.on("change", () => {
  console.log(collection);
});

collection.fetch();

The first thing I have to do is provide some types for the Collection itself and the first type will be a reference to the User class like so:

import { User, UserProps } from "./models/User";
import { Collection } from "./models/Collection";

const collection = new Collection<User>("http://localhost:3000/users");

collection.on("change", () => {
  console.log(collection);
});

collection.fetch();

The second argument will be a description to the array of json objects I get back:

import { User, UserProps } from "./models/User";
import { Collection } from "./models/Collection";

const collection = new Collection<User, UserProps>(
  "http://localhost:3000/users"
);

collection.on("change", () => {
  console.log(collection);
});

collection.fetch();

So the first time is the class that I will eventually produce, which is User and the second is going to be the description of the array of json objects I get back from the API which is UserProps. Then as the second argument I have to pass in a function that takes in an argument of structure UserProps and return an instance of a user like so:

import { User, UserProps } from "./models/User";
import { Collection } from "./models/Collection";

const collection = new Collection<User, UserProps>(
  "http://localhost:3000/users",
  (json: UserProps) => User.buildUser(json)
);

collection.on("change", () => {
  console.log(collection);
});

collection.fetch();

So heading over to my browser again I will do a quick test:

Screen Shot 2021-08-09 at 8 46 28 PM

So I got the collection again and I still have inside my models property an array of records where everyone of them is an instance of a user that has the appropriate data attached to it.

Now the problem here is that I probably do not want to do this customization, every single time that I have to create a new collection of users. Every single time I create a collection of users, I don't want to have to pass in this url:

const collection = new Collection<User, UserProps>(
  "http://localhost:3000/users",
  (json: UserProps) => User.buildUser(json)
);

And I dont want to have to define the deserialize function:

(json: UserProps) => User.buildUser(json)

So I think I need to figure out a better place to put this code than inside my index.ts file.

⚠️ **GitHub.com Fallback** ⚠️