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.