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 →