Skip to content

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 →