Start learning free

LESSON 565 OF 573 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Payment Fees in TypeScript: Abstract Classes

Practice O(1) Read the full explanation

A shop takes payments by card, by PayPal and in cash. Every payment has an amount, in cents, and a total: the amount plus a processing fee. The total is worked out the same way for every payment; only the fee depends on the kind of payment. That is what an abstract class is for: it holds the shared part, says that every payment has a fee, and leaves the fee itself to each kind of payment.

  • The abstract class Payment is built from an amount, in cents, and keeps it. Its method fee() is abstract: every payment has a fee, but Payment can't say what it is. Its method total() is concrete: it returns the amount plus fee().
  • Card, PayPal and Cash inherit from Payment, keep its total() as it is, and each writes its own fee():
  • Card pays 2%: its fee is amount × 2 / 100.
  • PayPal pays 3% plus 30 cents: its fee is amount × 3 / 100 + 30.
  • Cash costs nothing: its fee is 0.

In TypeScript, Payment is an abstract class and fee is declared abstract fee(): number;, with no body. Each subclass extends Payment, inherits the constructor and total, and writes its own fee. / keeps the fraction, so round the fee down with Math.floor.

Every division rounds down: a card payment of 1999 cents has a fee of 39, not 39.98.

The starter code already has every class and every method header: declare the field and the abstract method, and write the bodies. For each test, the harness creates the payments in the order given and prints two lines: each payment's total, total(), separated by spaces, and then the sum of all the fees. It calls them through the abstract type Payment, never through Card, PayPal or Cash.

 

Example 1:

Input:
    new Card(1999)
    new PayPal(500)
    new Cash(700)
Output:
    2038 545 700
    84
Explanation: The card's fee is 1999 × 2 / 100 = 39 (39.98 rounded down), so its total is 2038. PayPal's fee is 500 × 3 / 100 + 30 = 45, total 545. Cash has no fee: total 700. The fees add up to 39 + 45 + 0 = 84.

Example 2:

Input:
    new Card(49)
    new PayPal(33)
    new Card(50)
Output:
    49 63 51
    31
Explanation: 49 × 2 / 100 is 0.98, rounded down to 0. PayPal's 33 × 3 / 100 is 0.99, rounded down to 0, plus 30. The second card's 50 × 2 / 100 is exactly 1. The fees are 0 + 30 + 1 = 31.

Example 3:

Input:
    new Card(0)
    new PayPal(0)
    new Cash(0)
Output:
    0 30 0
    30
Explanation: Amounts can be 0. PayPal still adds its 30 cents.

 

Constraints:

  • Every test has between 1 and 100 payments.
  • Every amount is an integer from 0 to 106 cents.

Note:

Every fee and every total takes O(1) time.

STEP 1 OF 7
    Ctrl + Enter to check

    TYPESCRIPT · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Payment Fees in TypeScript, explained

    Problem overview

    A shop takes payments by card, by PayPal and in cash. Every payment has an amount in cents and a total, which is the amount plus a processing fee. The total is worked out the same way for every payment. Only the fee differs by kind:

    • Card: 2% of the amount, rounded down;
    • PayPal: 3% of the amount, rounded down, plus 30 cents;
    • Cash: no fee.

    You write the abstract class Payment, which keeps the amount, declares an abstract fee() and has a concrete total(), and the three subclasses that each write their own fee(). For a card payment of 1999 cents, the fee is 39 (the exact value is 39.98), and the total is 2038.

    Approach: share the total, leave the fee open

    The rule "total equals amount plus fee" is the same for all three payment kinds, so it is written once, in Payment. What a fee is depends on the kind, and Payment can't know that. An abstract method solves this: abstract fee(): number; says that every payment has a fee and leaves the body out. Each subclass must then provide it.

    When total() calls this.fee(), the version of the object's own class runs: a card's fee for a card, a PayPal fee for a PayPal payment. The total is written once and works for every kind, including kinds added later.

    An abstract class can't be created directly. Nobody can write new Payment(500); only new Card(500) and the other subclasses work. Each fee and each total takes O(1) time.

    Step by step

    1. The field. Declare amount: number in Payment. It lives in the parent once, and all three subclasses inherit it.
    2. The constructor. Store the parameter in this.amount. The subclasses don't write a constructor, so they inherit this one: new Card(1999) runs Payment's constructor.
    3. The abstract method. Write abstract fee(): number; with a semicolon in place of a body.
    4. The total. total() returns this.amount + this.fee().
    5. The card. The fee is Math.floor(this.amount * 2 / 100). Multiply first, then divide, then round down once.
    6. PayPal. The same shape with 3, plus 30 outside the rounding. The 30 cents are whole already and come on top, even for an amount of 0.
    7. Cash. fee() returns 0. Cash must still write the method, because it is abstract in Payment.

    The solution in TypeScript

    abstract class Payment {
        amount: number;
    
        constructor(amount: number) {
            this.amount = amount;
        }
    
        abstract fee(): number;
    
        total(): number {
            return this.amount + this.fee();
        }
    }
    
    class Card extends Payment {
        fee(): number {
            return Math.floor(this.amount * 2 / 100);
        }
    }
    
    class PayPal extends Payment {
        fee(): number {
            return Math.floor(this.amount * 3 / 100) + 30;
        }
    }
    
    class Cash extends Payment {
        fee(): number {
            return 0;
        }
    }
    

    TypeScript details worth knowing:

    • abstract is a compile-time rule. The compiler refuses new Payment(5) with Cannot create an instance of an abstract class, and refuses a subclass that leaves fee out with Non-abstract class 'Cash' does not implement inherited abstract member fee from class 'Payment'.
    • Errors don't stop the JavaScript. The compiler still writes the JavaScript, and a Cash class without fee compiles to a class with no such method. Calling total() on it fails at run time with this.fee is not a function.
    • Division keeps the fraction. 1999 * 2 / 100 is 39.98, and Math.floor turns it into 39. There is no integer division operator, so you round explicitly.
    • Inherited constructor. A subclass without a constructor gets its parent's, with the same parameters.

    Seeing it run

    function show(ps: Payment[]) {
        console.log(ps.map(p => p.total()).join(" "));
        console.log(ps.reduce((s, p) => s + p.fee(), 0));
    }
    show([new Card(1999), new PayPal(500), new Cash(700)]);
    show([new Card(49), new PayPal(33), new Card(50)]);
    show([new Card(0), new PayPal(0), new Cash(0)]);
    2038 545 700
    84
    49 63 51
    31
    0 30 0
    30

    The first line of each pair lists the totals and the second the sum of the fees. In the second group, 49 cents on a card is a fee of 0 (0.98 rounded down), and 33 cents on PayPal is 0 plus 30. A card payment of 50 cents pays exactly 1. Zero cents on PayPal still costs 30.

    Adding a new kind of payment

    Because total() only calls this.fee(), a new payment kind needs one small class. A bank transfer with a flat fee of 25 cents would look like this, in a file of your own:

    class Bank extends Payment {
        fee(): number {
            return 25;
        }
    }
    const list: Payment[] = [new Bank(1000), new Bank(0)];
    console.log(list.map(p => p.total()).join(" "));
    1025 25

    Nothing in Payment changes. The array is typed Payment[], which is the abstract type, and each element answers total() through its own fee(). This is the way the lesson's tests use your classes too: through the abstract type, never through Card, PayPal or Cash directly.

    Edge cases and common mistakes

    • Dividing first. Math.floor(150 / 100) * 2 is 2, but Math.floor(150 * 2 / 100) is 3. The rounding must come after the multiplication.
    • Skipping Math.floor. The fee of a 1999-cent card payment would be 39.98 and the total 2038.98, which is not a whole number of cents.
    • Rounding the 30 cents too. Write Math.floor(this.amount * 3 / 100) + 30. The 30 sits outside the call.
    • Leaving fee out of Cash. It is abstract, so every concrete class has to write it, even if it only returns 0.
    • Writing total() in every subclass. That repeats the rule three times. Keep it in Payment; subclasses only supply fee.
    • Using the percentage as a decimal. Writing this.amount * 0.02 is another way to say 2%, but the lesson's form is simpler to reason about: the product this.amount * 2 is a whole number, and the only division and the only rounding come at the end.
    • Amount 0. It is valid. Card and cash give a total of 0, and PayPal gives 30.
    Keep going with TypeScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free