IPv4 ↔ IPv6 Converter — TypeScript source
Convert between IPv4 and IPv6 addresses both ways. Parse and validate addresses, expand and compress IPv6 to its canonical RFC 5952 form, map an IPv4 into IPv4-mapped and IPv4-compatible IPv6 (or any custom /96 prefix), and extract an embedded IPv4 back out.
This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
// IPv4 ↔ IPv6 conversion - pure, deterministic logic. No DOM, no React. Every
// parse function returns `null` (or '' for the string renderers) on invalid
// input instead of throwing, so the UI can show a graceful error. IPv6 text
// follows RFC 5952: lowercase hex, no leading zeros, the single longest run of
// zero groups collapsed to `::`, and a dotted-decimal tail only for
// IPv4-mapped (`::ffff:`) addresses.
/** Options for embedding an IPv4 octet quad into an IPv6 address. */
export interface Ipv4ToIpv6Options {
/**
* Embedding family. `mapped` (default) → `::ffff:a.b.c.d`; `compatible` →
* `::a.b.c.d`. Ignored when `prefix` is supplied.
*/
mode?: 'mapped' | 'compatible';
/**
* Optional custom high-96-bit prefix (a valid IPv6 string; its first 6 groups
* are used and its low 32 bits are overwritten by the IPv4). e.g. `64:ff9b::`
* yields a NAT64-style `64:ff9b::a.b.c.d`. Overrides `mode`.
*/
prefix?: string;
}
const HEX = /^[0-9a-fA-F]{1,4}$/;
const DEC3 = /^\d{1,3}$/;
/** True when `groups` is exactly eight integers in the 16-bit range. */
function isValidIpv6Groups(groups: unknown): groups is number[] {
return (
Array.isArray(groups) &&
groups.length === 8 &&
groups.every((v) => Number.isInteger(v) && v >= 0 && v <= 0xffff)
);
}
/** Lowercase hex for one 16-bit group, with no leading zeros. */
const hexGroup = (v: number): string => v.toString(16);
/**
* Parse a dotted-decimal IPv4 string into four octets, validating each is 0-255.
* Returns `null` for anything that is not exactly four numeric octets in range.
*/
export function parseIpv4(s: string): number[] | null {
const parts = s.trim().split('.');
if (parts.length !== 4) return null;
const octets: number[] = [];
for (const p of parts) {
if (!DEC3.test(p)) return null;
const n = Number(p);
if (n < 0 || n > 255) return null;
octets.push(n);
}
return octets;
}
/** Render four octets as `a.b.c.d`, or `''` if the octets are out of range. */
export function ipv4ToString(octets: number[]): string {
if (
!Array.isArray(octets) ||
octets.length !== 4 ||
!octets.every((o) => Number.isInteger(o) && o >= 0 && o <= 255)
) {
return '';
}
return octets.join('.');
}
/**
* Parse an IPv6 string (with `::` compression, hex groups, and an optional
* dotted-decimal IPv4 tail for mapped/compatible forms) into eight 16-bit
* groups. Returns `null` on any malformed input - never throws.
*/
export function parseIpv6(s: string): number[] | null {
const input = s.trim();
if (!input) return null;
if ((input.match(/::/g) || []).length > 1) return null;
const dc = input.indexOf('::');
if (dc >= 0) {
const before = input.slice(0, dc);
const after = input.slice(dc + 2);
const headTokens = before === '' ? [] : before.split(':');
const tailTokens = after === '' ? [] : after.split(':');
const head: number[] = [];
for (const g of headTokens) {
if (!HEX.test(g)) return null;
head.push(parseInt(g, 16));
}
const tail: number[] = [];
for (let i = 0; i < tailTokens.length; i++) {
const g = tailTokens[i];
if (i === tailTokens.length - 1 && g.includes('.')) {
const oct = parseIpv4(g);
if (!oct) return null;
tail.push((oct[0] << 8) | oct[1], (oct[2] << 8) | oct[3]);
} else {
if (!HEX.test(g)) return null;
tail.push(parseInt(g, 16));
}
}
const total = head.length + tail.length;
if (total >= 8) return null; // `::` must elide at least one group
return [...head, ...new Array(8 - total).fill(0), ...tail];
}
// No compression: split and parse, allowing a dotted-quad only in the last slot.
const tokens = input.split(':');
const groups: number[] = [];
for (let i = 0; i < tokens.length; i++) {
const g = tokens[i];
if (i === tokens.length - 1 && g.includes('.')) {
const oct = parseIpv4(g);
if (!oct) return null;
groups.push((oct[0] << 8) | oct[1], (oct[2] << 8) | oct[3]);
} else {
if (!HEX.test(g)) return null;
groups.push(parseInt(g, 16));
}
}
return groups.length === 8 ? groups : null;
}
/** True when the eight groups form an IPv4-mapped (`::ffff:`) address. */
const isMapped = (g: number[]): boolean =>
g[0] === 0 && g[1] === 0 && g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 0xffff;
/** True when the eight groups form an IPv4-compatible (`::`) address. */
const isCompatible = (g: number[]): boolean =>
g[0] === 0 && g[1] === 0 && g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 0;
/**
* Collapse the longest run (length ≥ 2) of zero groups into `::` (first run
* wins on ties) and strip leading zeros - RFC 5952 canonical text for pure-hex
* IPv6. Does not emit dotted-decimal; call `renderCanonical` for that.
*/
function compressGroups(groups: number[]): string {
let bestStart = -1;
let bestLen = 0;
let curStart = -1;
let curLen = 0;
for (let i = 0; i < groups.length; i++) {
if (groups[i] === 0) {
if (curStart < 0) curStart = i;
curLen++;
if (curLen > bestLen) {
bestLen = curLen;
bestStart = curStart;
}
} else {
curStart = -1;
curLen = 0;
}
}
if (bestLen < 2) return groups.map(hexGroup).join(':');
const before = groups.slice(0, bestStart).map(hexGroup).join(':');
const after = groups.slice(bestStart + bestLen).map(hexGroup).join(':');
return `${before}::${after}`;
}
/**
* Render a compressed high part followed by a dotted-decimal IPv4 tail. When the
* high part already ends in `::` (its zero run reaches the boundary) the IPv4
* attaches directly; otherwise a single `:` separates them - so `::ffff:` →
* `::ffff:a.b.c.d` and `::` → `::a.b.c.d`.
*/
function renderWithEmbeddedTail(high: number[], octets: number[]): string {
const highStr = compressGroups(high);
const ipv4 = octets.join('.');
return highStr.endsWith('::') ? `${highStr}${ipv4}` : `${highStr}:${ipv4}`;
}
/**
* Canonical RFC 5952 text for eight groups: a dotted-decimal tail for
* IPv4-mapped (`::ffff:`) addresses, otherwise pure compressed hex. The
* deprecated IPv4-compatible range (`::/96`) is NOT rendered dotted here - that
* would mis-render the unspecified (`::`) and loopback (`::1`) addresses as
* `::0.0.0.0` / `::0.0.0.1`. Compatible extraction is still available via
* `ipv6ToIpv4`; on-demand compatible generation via `ipv4ToIpv6` is untouched.
*/
function renderCanonical(groups: number[]): string {
if (isMapped(groups)) {
const octets = [(groups[6] >> 8) & 0xff, groups[6] & 0xff, (groups[7] >> 8) & 0xff, groups[7] & 0xff];
return renderWithEmbeddedTail(groups.slice(0, 6), octets);
}
return compressGroups(groups);
}
/** Render eight groups as canonical compressed IPv6, or `''` if invalid. */
export function ipv6ToString(groups: number[]): string {
if (!isValidIpv6Groups(groups)) return '';
return renderCanonical(groups);
}
/** Expand an IPv6 string to its full eight-group, four-hex-digit form; `''` if invalid. */
export function expandIpv6(s: string): string {
const g = parseIpv6(s);
if (!g) return '';
return g.map((v) => v.toString(16).padStart(4, '0')).join(':');
}
/** Compress an IPv6 string to its RFC 5952 canonical form; `''` if invalid. */
export function compressIpv6(s: string): string {
const g = parseIpv6(s);
if (!g) return '';
return renderCanonical(g);
}
/**
* Embed an IPv4 octet quad into an IPv6 address. By default produces the
* IPv4-mapped form `::ffff:a.b.c.d`; `mode: 'compatible'` yields `::a.b.c.d`;
* a `prefix` overrides both and places the IPv4 after any custom /96 prefix
* (e.g. `64:ff9b::a.b.c.d`). Returns `''` for invalid octets or prefix.
*/
export function ipv4ToIpv6(octets: number[], opts?: Ipv4ToIpv6Options): string {
if (
!Array.isArray(octets) ||
octets.length !== 4 ||
!octets.every((o) => Number.isInteger(o) && o >= 0 && o <= 255)
) {
return '';
}
const hi = (octets[0] << 8) | octets[1];
const lo = (octets[2] << 8) | octets[3];
if (opts?.prefix !== undefined) {
const p = parseIpv6(opts.prefix);
if (!p) return '';
return renderWithEmbeddedTail(p.slice(0, 6), octets);
}
if (opts?.mode === 'compatible') {
return renderWithEmbeddedTail([0, 0, 0, 0, 0, 0], octets);
}
return renderWithEmbeddedTail([0, 0, 0, 0, 0, 0xffff], octets);
}
/**
* Extract the embedded IPv4 from an IPv4-mapped (`::ffff:a.b.c.d`) or
* IPv4-compatible (`::a.b.c.d`) address, returning dotted-decimal or `null`
* when the address carries no embedded IPv4 (or is unparseable).
*/
export function ipv6ToIpv4(s: string): string | null {
const g = parseIpv6(s);
if (!g || !(isMapped(g) || isCompatible(g))) return null;
const octets = [(g[6] >> 8) & 0xff, g[6] & 0xff, (g[7] >> 8) & 0xff, g[7] & 0xff];
return octets.join('.');
}
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 →