October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk5 min

How to Model Loan Payments and Interest in TypeScript

A TypeScript implementation of level-payment annuity and equal-principal schedules, with matched inputs, row-by-row calculations, and explicit precision assumptions.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

An annuity schedule keeps the combined payment level, while an equal-principal schedule keeps the principal portion level and lets total payments decline. The TypeScript example below models both using the same principal, periodic rate, and term, retains precision during calculations, and formats amounts only when presenting the results.

What an amortization schedule shows

An amortization schedule breaks each installment into interest and principal, then tracks the remaining loan balance. For a typical fixed-rate mortgage, the combined principal-and-interest payment stays stable while the amounts allocated to interest and principal change over time, as the Consumer Financial Protection Bureau explains.

As an Amazon Associate I earn from qualifying purchases.

The examples here assume a nonnegative starting principal, a positive whole-number term, a fixed rate per period, and regular payment periods. They exclude fees, prepayments, and irregular payment dates. The caller supplies the periodic rate as a decimal: 0.05 means 5% per period, not 5. Convert an annual rate to a periodic rate only after deciding whether it is nominal or effective and selecting the payment frequency.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How the two repayment methods differ

Schedule feature Annuity (equal payment) Equal principal
Principal in each installment Starts smaller and increases as interest takes less of the fixed payment. Same scheduled principal amount each period.
Total installment Level under fixed-rate, regular-period assumptions. Starts higher and declines as interest falls with the balance.
Balance reduction Principal reduction accelerates over time. Principal falls by the same amount each period.
Trade-off More predictable installment amount. Higher initial installments and faster early principal reduction.
Interest over the term Generally more than equal principal for the same principal, positive fixed rate, and term. Generally less than an annuity schedule under those matched assumptions.

Oracle’s Loans User Guide describes equal-principal repayment as equal principal installments with interest varying over time. The interest difference follows from the balance path: equal-principal payments reduce the balance faster early, leaving less principal on which later interest accrues.

Calculate each schedule

Annuity: level combined payment

Let P be the opening principal, n the number of payment periods, and r the rate per period. When r is nonzero, the payment is P × r / (1 − (1 + r)−n). When the rate is zero, divide the principal evenly: P / n. For every period, calculate interest from the opening balance, subtract it from the payment to get principal repaid, and subtract that principal from the balance.

Equal principal: level principal component

Divide P by n to get scheduled principal per period. Calculate interest as the opening balance multiplied by r; add it to scheduled principal for the installment, then reduce the balance by scheduled principal. With a positive fixed rate, the interest amount and therefore the total installment decline as the balance falls.

Rank #2
TypeScript Programming Language - Software Engineer & Coder T-Shirt
  • TypeScript implements a superset of syntax for strictly typed development, facilitating deep static analysis and enhanced development environment integration. The compiler translates source into standard script formats, ensuring parity across any runtime.
  • TypeScript is ideal for front-end developers, full-stack engineers, and software architects who build large-scale web applications. It serves those looking to improve code excellence, reduce bugs through static checking, and maintain complex projects more.
  • Lightweight, Classic fit, Double-needle sleeve and bottom hem

TypeScript implementation

This example rejects negative rates and invalid principal or term inputs. It keeps full JavaScript number precision through the recurrence and rounds only when formatting output. It does not round each installment as a lender might post it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
type ScheduleRow = {
  period: number;
  openingBalance: number;
  payment: number;
  interest: number;
  principal: number;
  closingBalance: number;
};

type Schedule = {
  rows: ScheduleRow[];
  totalInterest: number;
};

function validateInputs(principal: number, periods: number, ratePerPeriod: number): void {
  if (!Number.isFinite(principal) || principal < 0) {
    throw new RangeError("principal must be finite and nonnegative");
  }
  if (!Number.isInteger(periods) || periods <= 0) {
    throw new RangeError("periods must be a positive integer");
  }
  if (!Number.isFinite(ratePerPeriod) || ratePerPeriod < 0) {
    throw new RangeError("ratePerPeriod must be finite and nonnegative");
  }
}

function annuitySchedule(
  principal: number,
  periods: number,
  ratePerPeriod: number
): Schedule {
  validateInputs(principal, periods, ratePerPeriod);

  const regularPayment = ratePerPeriod === 0
    ? principal / periods
    : principal * ratePerPeriod /
      (1 - Math.pow(1 + ratePerPeriod, -periods));

  let balance = principal;
  let totalInterest = 0;
  const rows: ScheduleRow[] = [];

  for (let period = 1; period <= periods; period++) {
    const openingBalance = balance;
    const interest = openingBalance * ratePerPeriod;
    const isFinalPeriod = period === periods;
    const payment = isFinalPeriod
      ? openingBalance + interest
      : regularPayment;
    const principalPaid = payment - interest;
    balance = isFinalPeriod ? 0 : openingBalance - principalPaid;
    totalInterest += interest;

    rows.push({
      period,
      openingBalance,
      payment,
      interest,
      principal: principalPaid,
      closingBalance: balance,
    });
  }

  return { rows, totalInterest };
}

function equalPrincipalSchedule(
  principal: number,
  periods: number,
  ratePerPeriod: number
): Schedule {
  validateInputs(principal, periods, ratePerPeriod);

  const scheduledPrincipal = principal / periods;
  let balance = principal;
  let totalInterest = 0;
  const rows: ScheduleRow[] = [];

  for (let period = 1; period <= periods; period++) {
    const openingBalance = balance;
    const principalPaid = period === periods
      ? openingBalance
      : scheduledPrincipal;
    const interest = openingBalance * ratePerPeriod;
    const payment = principalPaid + interest;
    balance = period === periods ? 0 : openingBalance - principalPaid;
    totalInterest += interest;

    rows.push({
      period,
      openingBalance,
      payment,
      interest,
      principal: principalPaid,
      closingBalance: balance,
    });
  }

  return { rows, totalInterest };
}

function formatCurrency(value: number, currency = "USD", locale = "en-US"): string {
  return new Intl.NumberFormat(locale, {
    style: "currency",
    currency,
  }).format(value);
}

Why the final row is adjusted

The final installment in the annuity function equals the remaining balance plus that period’s interest, so it closes the modeled balance at zero rather than carrying a floating-point residue. In the equal-principal function, the final principal amount is the remaining balance; this absorbs any tiny recurrence difference from repeated division. These are explicit mathematical closure rules before display formatting, not a policy for rounding installments to cents each period.

Compare the rows and totals

Use identical inputs to compare the methods. This example uses a principal of 200,000, 360 monthly periods, and a periodic rate of 0.07 / 12. It assumes that 7% is a nominal annual rate divided by 12; it is an illustration, not a quote or a complete mortgage model.

const principal = 200_000;
const periods = 360;
const monthlyRate = 0.07 / 12;

const annuity = annuitySchedule(principal, periods, monthlyRate);
const equalPrincipal = equalPrincipalSchedule(principal, periods, monthlyRate);

for (const period of [1, 2, periods]) {
  const annuityRow = annuity.rows[period - 1];
  const equalPrincipalRow = equalPrincipal.rows[period - 1];

  console.log({
    period,
    annuityPayment: formatCurrency(annuityRow.payment),
    annuityInterest: formatCurrency(annuityRow.interest),
    annuityPrincipal: formatCurrency(annuityRow.principal),
    annuityClosingBalance: formatCurrency(annuityRow.closingBalance),
    equalPrincipalPayment: formatCurrency(equalPrincipalRow.payment),
    equalPrincipalInterest: formatCurrency(equalPrincipalRow.interest),
    equalPrincipalPrincipal: formatCurrency(equalPrincipalRow.principal),
    equalPrincipalClosingBalance: formatCurrency(equalPrincipalRow.closingBalance),
  });
}

console.log({
  annuityInterestTotal: formatCurrency(annuity.totalInterest),
  equalPrincipalInterestTotal: formatCurrency(equalPrincipal.totalInterest),
});

For context, the CFPB’s 2024 edition of 12 CFR § 1026.43 includes a regulatory example of a $200,000, 30-year fixed-rate loan at 7 percent with a substantially equal monthly fully amortizing payment of $1,331. That amount is rounded, and the regulation specifies calculations using non-rounded values; it is an example, not a current quote or a universal payment promise. The code above calculates from the stated nominal-rate assumption and does not implement every regulatory or contract rule.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Currency precision and production boundaries

TypeScript’s number type follows JavaScript Number behavior; ECMAScript defines Number as double-precision binary floating point. The TypeScript Handbook distinguishes number from the integer type bigint, but neither fact makes ordinary decimal currency arithmetic exact. Intl.NumberFormat formats a value for display; it does not define how a lender posts or rounds each installment.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Choose and document a rounding policy that matches the use case. The example rounds for display only and makes the final mathematical row close at zero.
  • If the modeled contract posts rounded amounts each period, implement that specific posting rule and recompute the balance from posted amounts instead of silently rounding or retaining hidden precision.
  • For production financial software, consider integer minor units or decimal arithmetic where appropriate, and follow applicable contract and jurisdictional rules.

Real loan schedules can differ when they include irregular first or final periods, payment-date timing, fees, rate changes, prepayments, or a day-count convention. The CFPB notes that odd periods and schedule irregularities can cause minor variations in otherwise substantially equal payments in its 2024 regulation text. Treat a schedule as a calculation under explicit assumptions, not as a substitute for the loan’s governing terms.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
PC Slower Than It Used to Be?Free scan - under a minute

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.