TypeScript Doesn't Add a Base64 API — But It Makes Yours Safer
TypeScript is a type system over JavaScript. There's noType64.encode() — you use the underlying JavaScript primitives (Buffer in Node, btoa in browsers) with type annotations. What TypeScript does give you is a way to prevent an entire class of Base64 bugs at compile time using branded types, discriminated unions, and strict null checks.
The two primitives you'll use:
- Node.js:
Buffer.from(text, "utf-8").toString("base64")— need@types/nodeinstalled. Handles Unicode correctly by default. - Browser:
btoa(latin1String)— Unicode-unsafe. Must UTF-8 encode first viaTextEncoderorencodeURIComponent.
Node.js — The Standard Pattern
// tsconfig.json: "types": ["node"] or install @types/node
// import { Buffer } from 'buffer'; // Node 16+ (Buffer is also global)
function encodeBase64Node(text: string): string {
return Buffer.from(text, 'utf-8').toString('base64');
}
function decodeBase64Node(encoded: string): string {
return Buffer.from(encoded, 'base64').toString('utf-8');
}
// Usage
const text = 'Hello 世界 👋';
const encoded = encodeBase64Node(text);
console.log(encoded); // SGVsbG8g5LiW55WMIPCfkYs=
const decoded = decodeBase64Node(encoded);
console.log(decoded); // Hello 世界 👋
// One-liner
const inline = Buffer.from('Hello', 'utf-8').toString('base64');
console.log(inline); // SGVsbG8=Browser — The Unicode-Safe Pattern
// btoa() only accepts Latin-1. For Unicode, encode to UTF-8 bytes first.
function encodeBase64Browser(text: string): string {
// Modern approach — TextEncoder is standard in all evergreen browsers
const bytes = new TextEncoder().encode(text);
const binString = Array.from(bytes, (b) => String.fromCharCode(b)).join('');
return btoa(binString);
}
function decodeBase64Browser(encoded: string): string {
const binString = atob(encoded);
const bytes = Uint8Array.from(binString, (c) => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
// Usage
const text: string = 'Hello 世界 👋';
const encoded: string = encodeBase64Browser(text);
console.log(encoded); // SGVsbG8g5LiW55WMIPCfkYs=
const decoded: string = decodeBase64Browser(encoded);
console.log(decoded); // Hello 世界 👋Isomorphic Helper — Works in Node and Browser
// Detect environment at runtime; modern bundlers tree-shake the dead branch.
export function encodeBase64(text: string): string {
if (typeof Buffer !== 'undefined') {
return Buffer.from(text, 'utf-8').toString('base64');
}
const bytes = new TextEncoder().encode(text);
const binString = Array.from(bytes, (b) => String.fromCharCode(b)).join('');
return btoa(binString);
}
export function decodeBase64(encoded: string): string {
if (typeof Buffer !== 'undefined') {
return Buffer.from(encoded, 'base64').toString('utf-8');
}
const binString = atob(encoded);
const bytes = Uint8Array.from(binString, (c) => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
// Same call site works in Next.js SSR, Cloudflare Workers, browser, Node CLI
const encoded = encodeBase64('Hello 世界');
const decoded = decodeBase64(encoded);Type-Safe Encoded Strings with Branded Types
Once you encode a string, you want the type system to remember it's encoded — so you can't accidentally send raw text where encoded is required, or decode text that's not actually Base64. Branded types make this explicit:
// A branded type — structurally a string, nominally distinct.
export type Base64String = string & { readonly __brand: unique symbol };
export function encode(text: string): Base64String {
const result = typeof Buffer !== 'undefined'
? Buffer.from(text, 'utf-8').toString('base64')
: btoa(unescape(encodeURIComponent(text)));
return result as Base64String;
}
export function decode(encoded: Base64String): string {
return typeof Buffer !== 'undefined'
? Buffer.from(encoded, 'base64').toString('utf-8')
: decodeURIComponent(escape(atob(encoded)));
}
// Now the compiler catches misuse:
const encoded = encode('Hello');
const twice = encode(encoded); // ✅ works — encoded is still a string
decode(encoded); // ✅ works
decode('raw text'); // ❌ Type '"raw text"' is not assignable to Base64String
// Runtime validator with type predicate
export function isBase64(s: string): s is Base64String {
// RFC 4648 alphabet + optional padding
return /^[A-Za-z0-9+/]*={0,2}$/.test(s) && s.length % 4 === 0;
}
// Safe entry point from untrusted input
export function parseBase64(input: string): Base64String | null {
return isBase64(input) ? input : null;
}URL-Safe Base64 (JWT-Style)
export function encodeBase64Url(text: string): string {
const std = encodeBase64(text);
return std.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
export function decodeBase64Url(encoded: string): string {
// Restore standard alphabet
let s = encoded.replace(/-/g, '+').replace(/_/g, '/');
// Add padding back
while (s.length % 4 !== 0) s += '=';
return decodeBase64(s);
}
// JWT payload example
const payload = { user_id: 42, role: 'admin' as const };
const jwtPart = encodeBase64Url(JSON.stringify(payload));
console.log(jwtPart); // eyJ1c2VyX2lkIjo0Miwicm9sZSI6ImFkbWluIn0Encoding a File or Blob in the Browser
// Promise-based helper — File or Blob → Base64 string (no data URL prefix)
export function fileToBase64(file: File | Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const dataUrl = reader.result as string;
// Strip "data:image/png;base64," prefix
const base64 = dataUrl.split(',')[1] ?? '';
resolve(base64);
};
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
}
// Usage in a React/Next.js form
async function handleFileUpload(file: File): Promise<void> {
const base64: string = await fileToBase64(file);
await fetch('/api/upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
mimeType: file.type,
contents: base64,
}),
});
}HTTP Basic Auth Header — Fully Typed
interface Credentials {
readonly user: string;
readonly password: string;
}
function basicAuthHeader({ user, password }: Credentials): string {
const encoded = encodeBase64(`${user}:${password}`);
return `Basic ${encoded}`;
}
// Typed fetch wrapper
async function callProtectedAPI<T>(creds: Credentials): Promise<T> {
const res = await fetch('https://api.example.com/protected', {
headers: { Authorization: basicAuthHeader(creds) },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
}
// Usage
interface UserResponse { id: number; name: string; }
const data = await callProtectedAPI<UserResponse>({
user: 'admin',
password: 'secret123',
});Common Pitfalls in TypeScript Base64 Code
- Assuming
btoahandles Unicode— TypeScript can't catch this.btoa("Hello 世界")throwsInvalidCharacterErrorat runtime. Always UTF-8 encode first withTextEncoderorencodeURIComponent. - Buffer in browser bundles — Using
Bufferin frontend code without a polyfill breaks. Modern bundlers (Vite, Next.js) handle this, but hand-rolled webpack configs often ship broken code. Check your bundle forBufferusage after building. - Loose
@types/nodeversions — Older@types/nodetypesBuffer.fromas returning a plainBufferwithout narrowing on the second arg. Upgrade to a version matching your Node runtime for proper type inference. - Casting
as stringwithout validation — When reading from external APIs, casting an unknown value tostringwithassilences the compiler but doesn't validate at runtime. Use a runtime type guard likeisBase64above before decoding. - Missing padding on JWT decode — JWT parts are URL-safe Base64 without padding. Passing them to standard
atoborBuffer.from(..., "base64")can fail on some runtimes. Add padding back with thewhile s.length % 4pattern first.
Command Line — TypeScript One-Liner
# Using tsx or ts-node
npx tsx -e "console.log(Buffer.from('Hello', 'utf-8').toString('base64'))"
# Output: SGVsbG8=
# Or just Deno (types work out of the box)
deno eval "console.log(btoa('Hello'))"
# System base64 (independent of TS)
echo -n "Hello" | base64Key Facts
- Language:
- TypeScript 5.4+ (works from 4.x, uses no new features)
- Node API:
- Buffer.from(text, 'utf-8').toString('base64') — needs @types/node
- Browser API:
- btoa(latin1) — must UTF-8 encode Unicode first with TextEncoder
- Isomorphic pattern:
- Runtime typeof Buffer check, tree-shaken by modern bundlers
- Type safety:
- Branded type Base64String prevents raw/encoded mix-ups at compile time
- Validation:
- Runtime guard: /^[A-Za-z0-9+/]*={0,2}$/ + length % 4 === 0
- JWT variant:
- Post-process: + → -, / → _, strip trailing =
- Zero deps:
- No npm package needed — built into Node and every browser
Related Base64 Tools
- Base64 Encode Online — general-purpose browser encoder
- Base64 Encode in JavaScript — plain JS Node.js and browser
- Base64 Encode in Python — Python 3 equivalent
- Base64 Encode in Go — Go encoding/base64
- Base64 Encode in Java — java.util.Base64
- URL-Safe Base64 — cross-language URL encoding
- JWT Debugger — inspect JWT tokens