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.