Extending with Generic Constraints - ldco2016/microurb_web_framework GitHub Wiki

I will ensure class View does not have any reference to User inside of it:

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

export abstract class View {
  constructor(public parent: Element, public model: User) {
    this.bindModel();
  }

  abstract eventsMap(): { [key: string]: () => void };
  abstract template(): string;

  bindModel(): void {
    this.model.on("change", () => {
      this.render();
    });
  }

  bindEvents(fragment: DocumentFragment): void {
    const eventsMap = this.eventsMap();

    for (let eventKey in eventsMap) {
      const [eventName, selector] = eventKey.split(":");

      fragment.querySelectorAll(selector).forEach((element) => {
        element.addEventListener(eventName, eventsMap[eventKey]);
      });
    }
  }

  render(): void {
    this.parent.innerHTML = "";

    const templateElement = document.createElement("template");
    templateElement.innerHTML = this.template();

    this.bindEvents(templateElement.content);

    this.parent.append(templateElement.content);
  }
}

A reference to User means that this class can only ever be used with a model of type User and chances are I am going to want to model out comments, posts and so on.

So I will first delete the import statement for User at the top and then I will look through the file and ensure there is only one reference to User inside there and I will then turn View into a generic class like so:

export abstract class View<T> {
  constructor(public parent: Element, public model: T) {
    this.bindModel();
  }

  abstract eventsMap(): { [key: string]: () => void };
  abstract template(): string;

  bindModel(): void {
    this.model.on("change", () => {
      this.render();
    });
  }

  bindEvents(fragment: DocumentFragment): void {
    const eventsMap = this.eventsMap();

    for (let eventKey in eventsMap) {
      const [eventName, selector] = eventKey.split(":");

      fragment.querySelectorAll(selector).forEach((element) => {
        element.addEventListener(eventName, eventsMap[eventKey]);
      });
    }
  }

  render(): void {
    this.parent.innerHTML = "";

    const templateElement = document.createElement("template");
    templateElement.innerHTML = this.template();

    this.bindEvents(templateElement.content);

    this.parent.append(templateElement.content);
  }
}

As soon as I do this I am going to get another error around this.model.on(). This happens with generics often. Anytime we attempt to use a generic, TypeScript is still going to go around and attempt to check all the different references I have on properties and methods on different objects. So in this case I am saying model is going to be of type T. So TypeScript has no clue what different properties and methods model might have and later on I have this on() method on model and TypeScript is complaining saying that I dont have a property called on() on type T, because we don't know what type T is, so type T might have a property of on(), but it might not.

So to fix this, I have to add a generic constraint which will guarantee to TypeScript that type T will have a certain set of properties and methods tied to it by building up an interface in the file that has all the properties and methods that I would expect type T to have. In this case, type T will be the type of my model. So really the interface will describe the different properties and methods the model should have like so:

interface ModelForView {
  on(eventName: string, callback: () => void): void;
}

export abstract class View<T extends ModelForView> {
  constructor(public parent: Element, public model: T) {
    this.bindModel();
  }

  abstract eventsMap(): { [key: string]: () => void };
  abstract template(): string;

  bindModel(): void {
    this.model.on("change", () => {
      this.render();
    });
  }

  bindEvents(fragment: DocumentFragment): void {
    const eventsMap = this.eventsMap();

    for (let eventKey in eventsMap) {
      const [eventName, selector] = eventKey.split(":");

      fragment.querySelectorAll(selector).forEach((element) => {
        element.addEventListener(eventName, eventsMap[eventKey]);
      });
    }
  }

  render(): void {
    this.parent.innerHTML = "";

    const templateElement = document.createElement("template");
    templateElement.innerHTML = this.template();

    this.bindEvents(templateElement.content);

    this.parent.append(templateElement.content);
  }
}

The error message goes away.