Compound Interest Calculator — TypeScript source
Project investment growth with any compounding frequency, scheduled contributions and inflation adjustment. Yearly breakdown, growth curve and CSV export — runs entirely in your browser.
This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Pure compound-interest logic — no React, no DOM. Deterministic and
// side-effect free. Invalid input returns null instead of throwing.
export interface InterestInput {
principal: number;
annualRatePct: number;
years: number;
/** 1 | 2 | 4 | 12 | 365. */
compoundsPerYear: number;
contribution?: { amount: number; perYear: number };
inflationPct?: number;
}
export interface InterestRow {
year: number;
startBalance: number;
contributed: number;
interest: number;
endBalance: number;
}
export interface InterestResult {
rows: InterestRow[];
summary: {
finalBalance: number;
totalContributed: number;
totalInterest: number;
inflationAdjustedFinal: number | null;
};
}
const VALID_COMPOUNDS = new Set([1, 2, 4, 12, 365]);
/** Round to 2 decimals, absorbing binary float noise (percentage.ts pattern). */
function round2(n: number): number {
return Math.round((n + Number.EPSILON) * 100) / 100;
}
/**
* Project growth with a monthly simulation. The monthly rate is the exact
* equivalent of the stated compounding frequency — (1 + r/n)^(n/12) − 1 — so
* the effective annual rate is preserved for every supported frequency and
* contributions apply naturally per month (amount × perYear / 12).
*/
export function project(input: InterestInput): InterestResult | null {
const { principal, annualRatePct, years, compoundsPerYear, contribution, inflationPct } = input;
const finite =
Number.isFinite(principal) &&
Number.isFinite(annualRatePct) &&
Number.isFinite(years) &&
Number.isFinite(compoundsPerYear) &&
(inflationPct === undefined || Number.isFinite(inflationPct)) &&
(contribution === undefined ||
(Number.isFinite(contribution.amount) && Number.isFinite(contribution.perYear)));
if (!finite) return null;
if (principal < 0 || years < 0 || !Number.isInteger(years)) return null;
if (!VALID_COMPOUNDS.has(compoundsPerYear)) return null;
if (contribution !== undefined && (contribution.amount < 0 || contribution.perYear <= 0)) {
return null;
}
const monthlyRate =
Math.pow(1 + annualRatePct / 100 / compoundsPerYear, compoundsPerYear / 12) - 1;
const monthlyContribution = contribution ? (contribution.amount * contribution.perYear) / 12 : 0;
const months = years * 12;
let balance = principal;
const rows: InterestRow[] = [];
for (let year = 1; year <= years; year++) {
const start = balance;
for (let m = 0; m < 12; m++) {
balance = balance * (1 + monthlyRate) + monthlyContribution;
}
const contributed = monthlyContribution * 12;
rows.push({
year,
startBalance: round2(start),
contributed: round2(contributed),
interest: round2(balance - start - contributed),
endBalance: round2(balance),
});
}
const totalContributed = monthlyContribution * months;
const finalBalance = balance;
const inflationAdjustedFinal =
inflationPct !== undefined && Number.isFinite(inflationPct)
? round2(finalBalance / Math.pow(1 + inflationPct / 100, years))
: null;
if (years === 0) {
// A flat projection still gets its single row so tables never render empty.
rows.push({
year: 0,
startBalance: round2(principal),
contributed: 0,
interest: 0,
endBalance: round2(principal),
});
}
return {
rows,
summary: {
finalBalance: round2(finalBalance),
totalContributed: round2(totalContributed),
totalInterest: round2(finalBalance - principal - totalContributed),
inflationAdjustedFinal,
},
};
}
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 →