Skip to content

Credit Card Validator — TypeScript source

Validate credit card numbers with the Luhn checksum and detect the issuing brand (Visa, Mastercard, Amex, Discover, Diners, JCB, UnionPay, Maestro). 100% client-side - nothing is transmitted.

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

// Pure credit-card validation logic - no React, no DOM, deterministic.
// Sanitizes to digits, runs the Luhn checksum, detects the issuing brand by
// prefix + length, and groups the number for display. Never throws.

export interface CardInfo {
  input: string;
  sanitized: string; // digits only
  valid: boolean; // digits present, sane length, and Luhn passes
  luhnValid: boolean; // Luhn checksum passes
  brand: string | null; // null when unknown
  formatted: string; // grouped per brand spacing
}

/** Luhn checksum. Empty or non-digit input → false. */
export function luhnCheck(digits: string): boolean {
  if (!/^\d*$/.test(digits) || digits.length === 0) return false;
  let sum = 0;
  let shouldDouble = false;
  for (let i = digits.length - 1; i >= 0; i--) {
    let d = digits.charCodeAt(i) - 48;
    if (shouldDouble) {
      d *= 2;
      if (d > 9) d -= 9;
    }
    sum += d;
    shouldDouble = !shouldDouble;
  }
  return sum % 10 === 0;
}

/** Detect the card brand by prefix + length. Returns null when unknown. */
export function detectBrand(digits: string): string | null {
  if (!digits) return null;
  const tests: Array<{ name: string; re: RegExp; lengths: number[] }> = [
    { name: 'Visa', re: /^4/, lengths: [13, 16, 19] },
    {
      name: 'Mastercard',
      re: /^(5[1-5]|2(2[2-9]|[3-6]\d|7[01]|720))/,
      lengths: [16],
    },
    { name: 'American Express', re: /^3[47]/, lengths: [15] },
    {
      name: 'Discover',
      re: /^(6011|65|64[4-9]|622(12[6-9]|1[3-9]\d|[2-8]\d\d|9([01]\d|2[0-5])))/,
      lengths: [16, 19],
    },
    {
      name: 'Diners Club',
      re: /^(30[0-5]|3095|36|38|39|54|55)/,
      lengths: [14, 16],
    },
    { name: 'JCB', re: /^35(2[89]|[3-8]\d)/, lengths: [16, 17, 18, 19] },
    { name: 'UnionPay', re: /^62/, lengths: [16, 17, 18, 19] },
    { name: 'Maestro', re: /^(50|56|57|58|6)/, lengths: [12, 13, 14, 15, 16, 17, 18, 19] },
  ];
  for (const t of tests) {
    if (t.re.test(digits) && t.lengths.includes(digits.length)) return t.name;
  }
  return null;
}

/** Group the number per brand spacing rules. */
export function formatCard(digits: string, brand: string | null): string {
  if (!digits) return '';
  if (brand === 'American Express') return group(digits, [4, 6, 5]);
  if (brand === 'Diners Club' && digits.length === 14) return group(digits, [4, 6, 4]);
  return digits.replace(/(.{4})(?=.)/g, '$1 ').trim();
}

function group(digits: string, widths: number[]): string {
  const out: string[] = [];
  let i = 0;
  for (const w of widths) {
    if (i >= digits.length) break;
    out.push(digits.slice(i, i + w));
    i += w;
  }
  if (i < digits.length) out.push(digits.slice(i));
  return out.join(' ');
}

/** Validate a card number. Always returns CardInfo; never throws. */
export function validateCard(input: string): CardInfo {
  const sanitized = (input || '').replace(/\D+/g, '');
  const brand = detectBrand(sanitized);
  const luhnValid = sanitized.length >= 12 && luhnCheck(sanitized);
  const valid = luhnValid;
  return {
    input: input ?? '',
    sanitized,
    valid,
    luhnValid,
    brand,
    formatted: formatCard(sanitized, brand),
  };
}

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 →