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
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 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.