Problem overview
An app tells its users about news by email or by SMS. The code that sends the news shouldn't care which kind a user picked; it only needs to know that every notifier can send a message. That promise is an interface. You write:
- the interface
Notifier, with one method,send(message), that returns the text that would be sent; EmailNotifier(address), whosesendreturns"To ", the address,": "and the whole message;SmsNotifier(phone), whosesendreturns"SMS ", the phone,": "and only the first 20 characters of the message;- a method
notifyAll(notifiers, message)in the classSolution, which returns the texts all the notifiers send, in the same order.
For an email notifier for ana@mail.com and an SMS notifier for 0712345678, the message "Meeting at 5" gives To ana@mail.com: Meeting at 5 and SMS 0712345678: Meeting at 5. The message has 12 characters, so the SMS sends all of it.
Approach: program to the interface
An interface lists method names and their types, with no bodies. A class that says implements Notifier promises to have those methods, and the compiler checks the promise. notifyAll then takes a Notifier[]: it calls send on each element and never asks which class the element is. A third kind of notifier, say a push notification, would work with notifyAll without any change to it.
The loop visits each notifier once, so notifyAll takes time proportional to the number of notifiers (plus the cost of each send), and the texts it returns keep the order of the input.
Step by step
- The interface. Inside
Notifier, write only the signaturesend(message: string): string;, ending in a semicolon. - The email's address. Declare a
privatefieldaddressof typestring, so only the class's own methods use it, and store the constructor's parameter inthis.address. - The email's
send. Join"To ", the address,": "and the whole message. - The SMS's phone. The same two pieces for
phone: aprivatefield, then the constructor storing it. - The SMS's
send. Join"SMS ", the phone,": "andmessage.slice(0, 20).slicestops at the end of the string, so a short message is kept whole. notifyAll. Create an emptystring[]withconst, loop over the notifiers withfor...of, pushnotifier.send(message)and return the array. With no notifiers the loop doesn't run and the empty array comes back.
The solution in TypeScript
interface Notifier {
send(message: string): string;
}
class EmailNotifier implements Notifier {
private address: string;
constructor(address: string) {
this.address = address;
}
send(message: string): string {
return "To " + this.address + ": " + message;
}
}
class SmsNotifier implements Notifier {
private phone: string;
constructor(phone: string) {
this.phone = phone;
}
send(message: string): string {
return "SMS " + this.phone + ": " + message.slice(0, 20);
}
}
class Solution {
notifyAll(notifiers: Notifier[], message: string): string[] {
const texts: string[] = [];
for (const notifier of notifiers) {
texts.push(notifier.send(message));
}
return texts;
}
}
TypeScript details worth knowing:
- Interfaces exist only for the compiler. The compiled JavaScript has no trace of
Notifier: it is a type, not a value.implementsadds a check, and the classes work the same without it. - The check is real. If
EmailNotifiersaysimplements Notifierbut has nosend, the compiler reportsClass 'EmailNotifier' incorrectly implements interface 'Notifier'andProperty 'send' is missing in type 'EmailNotifier' but required in type 'Notifier'. privateis also a compile-time rule. Readinge.addressfrom outside givesProperty 'address' is private and only accessible within class 'EmailNotifier'. The compiler still writes the JavaScript, and there the field is an ordinary property, so the value would print.sliceand indexes.message.slice(0, 20)takes the characters at indexes 0 to 19. The end index is not included, which is why 20 gives 20 characters.- Strings are joined with
+. When one side is a string,+concatenates. Every piece here is a string already.
Seeing it run
const s = new Solution();
const ns: Notifier[] = [new EmailNotifier("ana@mail.com"), new SmsNotifier("0712345678")];
console.log(s.notifyAll(ns, "Meeting at 5").join("\n"));
console.log(s.notifyAll([new SmsNotifier("5550100"), new EmailNotifier("bob@work.org")], "Your order has shipped today").join("\n"));
const two = [new SmsNotifier("111"), new SmsNotifier("222")];
for (const m of ["Exactly twenty chars", "Exactly twenty chars!"]) console.log(s.notifyAll(two, m).join("\n"));
console.log(s.notifyAll([], "Hi"));
To ana@mail.com: Meeting at 5
SMS 0712345678: Meeting at 5
SMS 5550100: Your order has shipp
To bob@work.org: Your order has shipped today
SMS 111: Exactly twenty chars
SMS 222: Exactly twenty chars
SMS 111: Exactly twenty chars
SMS 222: Exactly twenty chars
[]
In the second call the message has 28 characters: the SMS keeps the first 20, Your order has shipp, and the email sends everything. The texts follow the order of the notifiers. In the third call, "Exactly twenty chars" has exactly 20 characters and is sent whole. The second message, with a ! at the end, has 21; the SMS cuts the !, so both messages produce the same SMS text.
Adding a third kind
To see why the interface pays off, write a third class in a file of your own:
class PushNotifier implements Notifier {
private device: string;
constructor(device: string) {
this.device = device;
}
send(message: string): string {
return "Push " + this.device + ": " + message.slice(0, 10);
}
}
console.log(new Solution().notifyAll([new PushNotifier("phone-1")], "A longer message here"));
[ 'Push phone-1: A longer m' ]
The lesson's notifyAll handles it unchanged. The compiler also guards the promise in both directions: a send that takes a number instead of a string, or that returns a number, is reported as not assignable to the interface's (message: string) => string.
Edge cases and common mistakes
- Checking the class. Writing
if (notifier instanceof SmsNotifier)insidenotifyAlldefeats the interface. Callsendand let each object do its own thing. - A message of exactly 20 characters.
slice(0, 20)keeps all 20. Using 19 or 21 as the end index is the usual off-by-one. - Cutting the email. Only the SMS shortens the message. The email sends it whole.
- No notifiers. The result is an empty array, not
undefined. - Lost order. Push the texts in the loop so the output follows the input order.
- A signature that doesn't match. If a class's
sendtakes different parameters than the interface's, the compiler flags it inimplements. Keepsend(message: string): stringexactly. - Missing separators. The texts are
"To "+ address +": "+ message: a forgotten space after the colon changes every line.
