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 →