Skip to content

Mortgage Calculator — TypeScript source

Compute monthly payments, the full amortization schedule and total interest, with extra-payment what-if analysis showing months and interest saved — runs entirely in your browser.

This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Pure mortgage amortization logic — no React, no DOM. Deterministic and
// side-effect free. Invalid input returns null instead of throwing.
//
// All money is tracked in integer cents (balances, interest accrual) so the
// TS and Go twins stay byte-identical; the scheduled payment stays exact
// (unrounded) and the final month clears the residual, which is how real
// amortization handles a cent-rounded quoted payment.

export interface MortgageInput {
  principal: number;
  annualRatePct: number;
  years: number;
  extraMonthly?: number;
  /** ISO yyyy-mm — drives schedule dates and payoffDate. */
  startDate?: string;
}

export interface MortgageRow {
  month: number;
  date: string;
  payment: number;
  interest: number;
  principalPart: number;
  extra: number;
  balance: number;
}

export interface MortgageResult {
  monthlyPayment: number;
  totalPaid: number;
  totalInterest: number;
  months: number;
  payoffDate: string | null;
  schedule: MortgageRow[];
  monthsSavedByExtra: number;
  interestSavedByExtra: number;
}

/** Round cents (or a cents-float) to a display dollar value. */
function centsToDollars(cents: number): number {
  return Math.round((cents / 100 + Number.EPSILON) * 100) / 100;
}

const ISO_MONTH = /^\d{4}-(0[1-9]|1[0-2])$/;

/** Add k months to an ISO yyyy-mm, rolling the year. */
function addMonths(iso: string, k: number): string {
  const y = Number(iso.slice(0, 4));
  const m = Number(iso.slice(5, 7));
  const idx = y * 12 + (m - 1) + k;
  const year = Math.floor(idx / 12);
  const month = (idx % 12) + 1;
  return `${year}-${String(month).padStart(2, '0')}`;
}

interface RunResult {
  schedule: MortgageRow[];
  months: number;
  totalInterestCents: number;
}

function run(
  principalCents: number,
  monthlyRate: number,
  payment: number,
  extraCents: number,
  startDate: string | null,
): RunResult {
  let balance = principalCents;
  let totalInterest = 0;
  let month = 0;
  const schedule: MortgageRow[] = [];
  while (balance > 0) {
    month += 1;
    const interest = Math.round(balance * monthlyRate);
    let principalPart = Math.min(payment - interest, balance);
    let extra = Math.min(extraCents, balance - principalPart);
    if (balance - principalPart - extra <= 0.5) {
      // Final month: clear the residual exactly instead of a dust payment.
      principalPart = balance;
      extra = 0;
    }
    balance -= principalPart + extra;
    totalInterest += interest;
    schedule.push({
      month,
      date: startDate ? addMonths(startDate, month - 1) : '',
      payment: centsToDollars(interest + principalPart + extra),
      interest: centsToDollars(interest),
      principalPart: centsToDollars(principalPart),
      extra: centsToDollars(extra),
      balance: centsToDollars(balance),
    });
  }
  return { schedule, months: month, totalInterestCents: totalInterest };
}

export function amortize(input: MortgageInput): MortgageResult | null {
  const { principal, annualRatePct, years, extraMonthly, startDate } = input;
  if (
    !Number.isFinite(principal) ||
    !Number.isFinite(annualRatePct) ||
    !Number.isFinite(years) ||
    (extraMonthly !== undefined && !Number.isFinite(extraMonthly))
  ) {
    return null;
  }
  if (principal <= 0 || years <= 0 || !Number.isInteger(years)) return null;
  if (extraMonthly !== undefined && extraMonthly < 0) return null;

  const validStart = startDate !== undefined && ISO_MONTH.test(startDate) ? startDate : null;

  const principalCents = Math.round(principal * 100);
  const extraCents = extraMonthly ? Math.round(extraMonthly * 100) : 0;
  const r = annualRatePct / 100 / 12;
  const n = years * 12;
  const payment = r === 0 ? principalCents / n : (principalCents * r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1);

  const primary = run(principalCents, r, payment, extraCents, validStart);
  const baseline = extraCents > 0 ? run(principalCents, r, payment, 0, null) : null;

  return {
    monthlyPayment: centsToDollars(payment),
    totalPaid: centsToDollars(primary.totalInterestCents + principalCents),
    totalInterest: centsToDollars(primary.totalInterestCents),
    months: primary.months,
    payoffDate: validStart && primary.schedule.length > 0 ? primary.schedule[primary.schedule.length - 1].date : null,
    schedule: primary.schedule,
    monthsSavedByExtra: baseline ? baseline.months - primary.months : 0,
    interestSavedByExtra: baseline
      ? centsToDollars(baseline.totalInterestCents - primary.totalInterestCents)
      : 0,
  };
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →