Q1You're adding PdfFile to a list that has a loop calling file.preview(). What do you need to add?
Polymorphism — Same Call, Different Behavior
Give each class a method with the same name, and one call works for all of them. Covers mixed arrays, instanceof checks, and duck typing.
Imagine a contact form that lists its attachments, showing a thumbnail for images and a play button for videos. If you store the type as a string and branch with if in the display function, you have to open that function and add to it every time you add a type.
This article covers polymorphism, where a method with the same name runs different code in each class, and duck typing, which doesn't care about inheritance at all.
Putting Display Differences in Classes — Methods with the Same Name
Suppose each attachment is an object like { kind: "video", fileName: "demo.mp4" }, and a function called previewOf compares kind. To add videos, you open previewOf, which also holds the branch for images, and add to it.
With polymorphism (giving separate classes a method with the same name, so the caller writes the same call and the code of the class that created the instance runs), you put each type's display in the child class's preview.
Once each child class overrides the parent's preview, the caller just writes preview() without checking which child class it has.
// Store the type as a string and branch with if in the display function
function previewOf(file) {
if (file.kind === "image") return `${file.fileName}: Show thumbnail`;
return `${file.fileName}: No preview`;
}
console.log(previewOf({ kind: "video", fileName: "demo.mp4" })); // demo.mp4: No preview
// Write a preview method with the same name in each type's class
class Attachment {
constructor(fileName) { this.fileName = fileName; }
preview() { return `${this.fileName}: No preview`; }
}
class ImageFile extends Attachment {
preview() { return `${this.fileName}: Show thumbnail`; }
}
class VideoFile extends Attachment {
preview() { return `${this.fileName}: Show play button`; }
}
console.log(new ImageFile("logo.png").preview()); // logo.png: Show thumbnail
console.log(new VideoFile("demo.mp4").preview()); // demo.mp4: Show play button
The first console.log printed "No preview" because previewOf doesn't have a "video" branch yet. With classes, whoever adds videos only needs to use the same name, preview, and doesn't have to remember which strings go in kind.
Calling a Mix of Types at Once — Arrays and for...of
The list arrives as a single array that mixes images, PDFs, and files of unknown type. If you put each one in its own variable and write out a preview() call for each, you'll have to add or remove calls every time the number of attachments changes.
You can put instances of different classes into one array. When you loop over it with for...of, the file.preview() in the loop body runs on an instance of a different class on each pass, and the call stays a single line.
When you add PdfFile for PDFs, you don't touch the loop.
// Attachment and ImageFile are defined as in the first example
// A type added later. The loop doesn't change
class PdfFile extends Attachment {
preview() { return `${this.fileName}: Show page 1`; }
}
const attachments = [
new ImageFile("logo.png"),
new PdfFile("manual.pdf"),
new Attachment("memo.txt"),
];
// The body is one line. On each pass, file holds an instance of a different class
for (const file of attachments) {
console.log(file.preview());
}
// logo.png: Show thumbnail
// manual.pdf: Show page 1
// memo.txt: No preview
memo.txt on the third pass was created from Attachment, so Attachment's own preview runs. If a new child class forgets to define preview, it falls back to this same output instead of throwing an error, so whenever you add a type, check its line in the list.
Calling Code Only One Type Has — Branching with instanceof
Next, the image rows in the list need a crop button. cropButton, which returns the button, is defined only in ImageFile, so calling file.cropButton() on every item in the loop throws a TypeError on the memo.txt pass.
instanceof returns true if the value on the left was created from the class on the right or one of its child classes. Check file instanceof ImageFile and call cropButton only on the passes where it's true.
Since every class has preview, you can call it directly without a check.
class Attachment {
constructor(fileName) { this.fileName = fileName; }
preview() { return `${this.fileName}: No preview`; }
}
class ImageFile extends Attachment {
preview() { return `${this.fileName}: Show thumbnail`; }
cropButton() { return `${this.fileName}: Show crop button`; } // Only ImageFile has this
}
const attachments = [new ImageFile("logo.png"), new Attachment("memo.txt")];
for (const file of attachments) {
console.log(file.preview()); // Every class has it, so call it without a check
if (file instanceof ImageFile) {
console.log(file.cropButton()); // Call it only on ImageFile passes
}
}
// logo.png: Show thumbnail
// logo.png: Show crop button
// memo.txt: No preview
The top row fails with TypeError: file.cropButton is not a function. If you branch on instanceof even for preview, you end up adding a check for every type, just like previewOf. Limit instanceof branches to methods that only some classes have.
Wrap instanceof in Parentheses When Using !
In !file instanceof ImageFile, !file is evaluated first and becomes false, so the check is really false instanceof ImageFile, which is always false. To say "not an ImageFile", wrap it in parentheses: !(file instanceof ImageFile).
Listing Values That Don't Inherit — Duck Typing
The list also needs to show folder links. A link is an object literal with a preview() method, and it doesn't inherit from Attachment. If you check instanceof Attachment to filter out old data without preview, such as { fileName: "old.doc" }, the link also gets false and is lumped in with the old data as "Can't display".
With duck typing (deciding how to handle a value based only on whether it has the methods you need, without checking its class or inheritance), you check typeof item.preview === "function".
A method call works whatever the class is, as long as the value to the left of the dot has a function with that name.
class Attachment {
constructor(fileName) { this.fileName = fileName; }
preview() { return `${this.fileName}: No preview`; }
}
// A link made without a class. It doesn't inherit from Attachment
const folderLink = {
fileName: "Quotes folder",
preview() { return `${this.fileName}: Open link`; },
};
console.log(folderLink instanceof Attachment); // false
// Decide by whether it has preview as a function, not by its class
for (const item of [new Attachment("memo.txt"), folderLink, { fileName: "old.doc" }]) {
if (typeof item.preview === "function") {
console.log(item.preview()); // memo.txt: No preview / Quotes folder: Open link
} else {
console.log(`${item.fileName}: Can't display`); // old.doc: Can't display
}
}
Since there's no instanceof check, a value whose preview function doesn't return a string also gets through. Make sure every value you put in the list has a preview() that returns a display string. The table below compares the three ways of calling used in this article.
| How You Call It | What It Checks | When to Use It |
|---|---|---|
| Call preview() directly | Nothing | Every element has preview |
| Branch on instanceof | Created from ImageFile or a child? | To call a method only that class has |
| Branch on typeof | Is preview a function? | To accept any value with preview |
in Returns true Even If preview Isn't a Function
"preview" in item is true even when the value is a string, as in { preview: "Coming soon" }. Calling item.preview() on that value throws TypeError: item.preview is not a function, so before calling it, use typeof to check that it's actually a function.
Knowledge Check
Answer each question one by one.
Q2For memo, created from Attachment, what is the result of !memo instanceof ImageFile?
Q3For an object literal link with a preview() method, what are link instanceof Attachment and typeof link.preview?