typescript
50 lines · 9 steps
Recursively masking sensitive data for logs
A key-pattern registry drives a recursive walk that redacts emails, cards, and secrets before anything hits your logs.
Explained by
highlit
1type Masker = (value: string) => string;
2
3const maskEmail: Masker = (value) => {
4 const [local, domain] = value.split("@");
5 if (!domain) return value;
6 const visible = local.slice(0, 2);
7 return `${visible}${"*".repeat(Math.max(local.length - 2, 1))}@${domain}`;
8};
9
10const maskCard: Masker = (value) => {
11 const digits = value.replace(/\D/g, "");
12 if (digits.length < 12) return value;
13 return `**** **** **** ${digits.slice(-4)}`;
14};
15
16const SENSITIVE_KEYS = new Map<RegExp, Masker>([
17 [/email|e_mail/i, maskEmail],
18 [/card|ccnum|creditCard/i, maskCard],
19 [/password|secret|token|apiKey/i, () => "[REDACTED]"],
20]);
21
22function resolveMasker(key: string): Masker | undefined {
23 for (const [pattern, masker] of SENSITIVE_KEYS) {
24 if (pattern.test(key)) return masker;
25 }
26 return undefined;
27}
28
29export function maskForLogging<T>(input: T, seen = new WeakSet<object>()): T {
30 if (Array.isArray(input)) {
31 return input.map((item) => maskForLogging(item, seen)) as unknown as T;
32 }
33
34 if (input !== null && typeof input === "object") {
35 if (seen.has(input)) return input;
36 seen.add(input);
37
38 const result: Record<string, unknown> = {};
39 for (const [key, value] of Object.entries(input)) {
40 const masker = resolveMasker(key);
41 result[key] =
42 masker && typeof value === "string"
43 ? masker(value)
44 : maskForLogging(value, seen);
45 }
46 return result as T;
47 }
48
49 return input;
50}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A registry of regex-to-transform pairs keeps masking rules declarative and easy to extend.
- 2A WeakSet of visited objects lets recursive traversal survive cyclic references without infinite loops.
- 3Masking by key name means new fields are protected automatically once they match an existing pattern.
Related explainers
php
<?php namespace App\Http\Requests\DataObjects;
Typed request DTOs in Laravel
data-transfer-object
validation
immutability
Intermediate
6 steps
typescript
import { Component } from '@angular/core'; import { RouterLink, RouterLinkActive } from '@angular/router'; import { NgFor } from '@angular/common';
Building an active-route navbar in Angular
routing
standalone-components
accessibility
Intermediate
6 steps
ruby
def render_table(rows) return "(no data)" if rows.empty? columns = rows.flat_map(&:keys).uniq
Rendering an ASCII table in Ruby
text-formatting
column-alignment
recursion
Intermediate
8 steps
typescript
interface ParsedName { first: string; middle: string; last: string;
Parsing human names into structured parts
parsing
string-manipulation
normalization
Intermediate
9 steps
typescript
import { useCallback, useEffect, useRef, useState } from "react"; interface UseResendCooldownOptions { cooldownSeconds?: number;
A resend cooldown hook in React
custom-hooks
timers
state-management
Intermediate
7 steps
typescript
import { Injectable, PipeTransform, ArgumentMetadata,
A custom validation pipe in NestJS
validation
dto
recursion
Intermediate
10 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/recursively-masking-sensitive-data-for-logs-explained-typescript-1b7d/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.