A lightweight, zero-dependency TypeScript library for masking phone numbers to protect personal information.
- π Privacy Compliant - Follows GDPR and data protection standards
- β¨ Lightweight - Under 2KB, zero dependencies
- π¦ TypeScript - Full type safety and IntelliSense support
- βοΈ Flexible - Extensive customization options
- π Universal - Supports all international phone formats (US, UK, EU, Asia, etc.)
- π― Locale-Agnostic - No assumptions about phone number format
- π Simple API - Easy to use with sensible defaults
npm install @ekaone/mask-phoneyarn add @ekaone/mask-phonepnpm add @ekaone/mask-phoneimport { maskPhone } from '@ekaone/mask-phone';
maskPhone('1234567890');
// Output: '******7890'
maskPhone('+62 812 3456 7890');
// Output: '+*********7890'import { maskPhone } from '@ekaone/mask-phone';
// Default masking (shows last 4 digits)
maskPhone('1234567890');
// Output: '******7890'
// Accepts number input
maskPhone(1234567890);
// Output: '******7890'
// Auto-strips formatting
maskPhone('+1-234-567-8901');
// Output: '+*******8901'
// Preserves international prefix
maskPhone('+1234567890');
// Output: '+******7890'Control how many digits to show at the start:
// Show first 3 digits (country/area code)
maskPhone('628123456789', { showFirst: 3 });
// Output: '628*****6789'
// Show first 2 digits
maskPhone('+1234567890', { showFirst: 2 });
// Output: '+1*****7890'
// Show only first digit
maskPhone('1234567890', { showFirst: 1, showLast: 0 });
// Output: '1*********'Control how many digits to show at the end:
// Show last 2 digits
maskPhone('1234567890', { showLast: 2 });
// Output: '********90'
// Show last 6 digits
maskPhone('628123456789', { showLast: 6 });
// Output: '******456789'
// Hide all digits (complete masking)
maskPhone('1234567890', { showFirst: 0, showLast: 0 });
// Output: '**********'Alternative names for clarity:
// showStart is alias for showFirst
maskPhone('1234567890', { showStart: 3 });
// Output: '123****7890'
// showEnd is alias for showLast
maskPhone('1234567890', { showEnd: 2 });
// Output: '********90'
// Can be mixed (showFirst takes priority)
maskPhone('1234567890', { showStart: 2, showEnd: 3 });
// Output: '12*****890'Change the masking character from the default *:
maskPhone('1234567890', { maskChar: 'β’' });
// Output: 'β’β’β’β’β’β’7890'
maskPhone('1234567890', { maskChar: 'X' });
// Output: 'XXXXXX7890'
maskPhone('1234567890', { maskChar: '#' });
// Output: '######7890'Maintain spaces, dashes, parentheses, and other separators from the input:
// Preserve US format
maskPhone('+1 (555) 123-4567', { preserveFormat: true });
// Output: '+* (***) ***-4567'
// Preserve international spacing
maskPhone('+62 812 3456 7890', { preserveFormat: true });
// Output: '+** *** **** 7890'
// Preserve dashes
maskPhone('+1-555-123-4567', { preserveFormat: true });
// Output: '+*-***-***-4567'
// Preserve dots
maskPhone('+62.812.3456.7890', { preserveFormat: true });
// Output: '+**.***.****.7890'Show specific character ranges:
// Show country code and last 4
maskPhone('628123456789', { visibleRanges: [[0, 2], [8, 11]] });
// Output: '628*****6789'
// Show only middle section
maskPhone('1234567890', { visibleRanges: [[3, 6]] });
// Output: '***4567***'
// Multiple non-contiguous ranges
maskPhone('+441234567890', {
visibleRanges: [[0, 2], [6, 8]]
});
// Output: '+44***456****'
// Works with preserveFormat
maskPhone('+1 (555) 123-4567', {
visibleRanges: [[0, 3], [12, 15]],
preserveFormat: true
});
// Output: '+1 (555) ***-****'Full control over masking logic:
// Mask every other character
maskPhone('1234567890', {
customMask: (char, idx) => idx % 2 === 0 ? '*' : char
});
// Output: '*2*4*6*8*0'
// Mask based on position
maskPhone('1234567890', {
customMask: (char, idx, phone) =>
idx < phone.length / 2 ? '*' : char
});
// Output: '*****67890'
// Conditional masking
maskPhone('1234567890', {
customMask: (char, idx) =>
['1', '3', '5', '7', '9'].includes(char) ? '*' : char
});
// Output: '*2*4*6*8*0'
// Works with preserveFormat
maskPhone('+1 (555) 123-4567', {
preserveFormat: true,
customMask: (char, idx) =>
char.match(/\d/) && idx % 2 === 0 ? 'X' : char
});
// Output: '+X (X5X) X2X-X5X7'Mix and match options for custom behavior:
// Common pattern: show country code + last 4
maskPhone('628123456789', {
maskChar: 'β’',
showFirst: 3,
showLast: 4
});
// Output: '628β’β’β’β’β’6789'
// Preserve format with custom mask
maskPhone('+1 (555) 123-4567', {
maskChar: 'X',
preserveFormat: true
});
// Output: '+X (XXX) XXX-4567'
// Everything combined
maskPhone('628123456789', {
maskChar: '#',
showFirst: 3,
showLast: 4
});
// Output: '628#####6789'Masks a phone number according to the provided options.
- input (
string | number) - The phone number to mask - options (
MaskOptions, optional) - Configuration options
| Option | Type | Default | Description |
|---|---|---|---|
maskChar |
string |
'*' |
Character used for masking |
showFirst |
number |
0 |
Number of digits to show at the beginning |
showLast |
number |
4 |
Number of digits to show at the end |
showStart |
number |
- | Alias for showFirst (for clarity) |
showEnd |
number |
- | Alias for showLast (for clarity) |
visibleRanges |
Array<[number, number]> |
- | Specific ranges to keep visible [[start, end], ...] |
preserveFormat |
boolean |
false |
Maintain original spacing/formatting from input |
customMask |
function |
- | Custom masking function (char, index, phone) => string |
- (
string) - The masked phone number
All types and interfaces exported from the package:
/**
* Phone masking options
* All options are optional and use flat structure for simplicity
*/
export interface MaskOptions {
/**
* Character used for masking
* @default '*'
* @example '#', 'X', 'β’'
*/
maskChar?: string;
/**
* Number of characters to show from the start
* @example 3 β '628*******'
*/
showFirst?: number;
/**
* Number of characters to show from the end (most common pattern)
* @default 4
* @example 4 β '******7890'
*/
showLast?: number;
/**
* Alias for showFirst (for clarity)
* @example 2 β '62********'
*/
showStart?: number;
/**
* Alias for showLast (for clarity)
* @example 4 β '******7890'
*/
showEnd?: number;
/**
* Specific ranges to keep visible
* Array of [startIndex, endIndex] (inclusive)
* @example [[0, 2], [8, 10]] β '628****789*'
*/
visibleRanges?: Array<[number, number]>;
/**
* Preserve formatting characters (spaces, dashes, parentheses, etc.)
* When true: '+1 (555) 123-4567' β '+* (***) ***-4567'
* When false: '+1 (555) 123-4567' β '**************' (strips then masks)
* @default false
*/
preserveFormat?: boolean;
/**
* Custom masking function for full control
* Overrides all other options
* @param char - Current character
* @param index - Position in the phone string
* @param phone - Full phone string
* @returns Masked character or original
* @example (char, idx) => idx % 2 === 0 ? '*' : char
*/
customMask?: (char: string, index: number, phone: string) => string;
}
/**
* Input type for phone parameter
* Accepts both string and number for flexibility
*/
export type PhoneInput = string | number;
/**
* Default masking options
*/
export const DEFAULT_OPTIONS: Required<Omit<MaskOptions, 'showFirst' | 'showStart' | 'showEnd' | 'visibleRanges' | 'customMask'>> = {
maskChar: '*',
showLast: 4,
preserveFormat: false,
};
/**
* Type guard to check if value is a valid phone input
*/
export function isValidPhoneInput(value: unknown): value is PhoneInput;The package exports the following members:
| Export | Type | Description |
|---|---|---|
maskPhone |
function |
Main masking function |
MaskOptions |
interface |
Configuration options interface (type-only) |
PhoneInput |
type |
Union type: string | number (type-only) |
DEFAULT_OPTIONS |
const |
Default configuration constants |
isValidPhoneInput |
function |
Type guard for runtime validation |
Import examples:
// Import only what you need (tree-shakeable)
import { maskPhone } from '@ekaone/mask-phone';
// Import with types (TypeScript)
import {
maskPhone,
type MaskOptions,
type PhoneInput
} from '@ekaone/mask-phone';
// Import everything
import {
maskPhone,
type MaskOptions,
type PhoneInput,
DEFAULT_OPTIONS,
isValidPhoneInput
} from '@ekaone/mask-phone';const phoneNumber = '+62 812 3456 7890';
const maskedPhone = maskPhone(phoneNumber, {
showFirst: 3,
preserveFormat: true
});
console.log(`Contact: ${maskedPhone}`);
// Output: "Contact: +62 *** **** 7890"const userPhone = '1234567890';
const displayPhone = maskPhone(userPhone);
console.log(`Phone: ${displayPhone}`);
// Output: "Phone: ******7890"function showVerificationTarget(phone: string) {
return maskPhone(phone, {
showLast: 4,
preserveFormat: true
});
}
const phone = '+1 (555) 123-4567';
console.log(`Code sent to: ${showVerificationTarget(phone)}`);
// Output: "Code sent to: +* (***) ***-4567"function maskPhoneBySecurityLevel(
phone: string,
level: 'low' | 'medium' | 'high'
) {
switch (level) {
case 'low':
return maskPhone(phone, { showFirst: 3, showLast: 4 });
case 'medium':
return maskPhone(phone, { showLast: 4 });
case 'high':
return maskPhone(phone, { showFirst: 0, showLast: 0 });
}
}
const phone = '628123456789';
console.log('Low: ', maskPhoneBySecurityLevel(phone, 'low'));
console.log('Medium:', maskPhoneBySecurityLevel(phone, 'medium'));
console.log('High: ', maskPhoneBySecurityLevel(phone, 'high'));
// Output:
// Low: 628*****6789
// Medium: ********6789
// High: ************const contacts = [
{ name: 'Alice', phone: '+1 (415) 555-2671' },
{ name: 'Bob', phone: '+44 20 7123 4567' },
{ name: 'Charlie', phone: '+62 812 3456 7890' }
];
contacts.forEach(contact => {
const masked = maskPhone(contact.phone, {
preserveFormat: true
});
console.log(`${contact.name}: ${masked}`);
});
// Output:
// Alice: +* (***) ***-2671
// Bob: +** ** **** 4567
// Charlie: +** *** **** 7890function logPhoneAccess(phone: string, action: string) {
const maskedPhone = maskPhone(phone, {
showFirst: 2,
showLast: 0
});
console.log(`[${new Date().toISOString()}] ${action}: ${maskedPhone}`);
}
logPhoneAccess('+1234567890', 'Phone number viewed');
// Output: "[2025-02-03T10:30:00.000Z] Phone number viewed: +1*********"const receiptPhone = '+62 812 3456 7890';
const formatted = maskPhone(receiptPhone, {
maskChar: 'β’',
preserveFormat: true
});
console.log(`Contact: ${formatted}`);
// Output: "Contact: +β’β’ β’β’β’ β’β’β’β’ 7890"Works seamlessly with all international phone formats without any locale assumptions:
// United States
maskPhone('+1 (415) 555-2671', { preserveFormat: true });
// Output: '+* (***) ***-2671'
// United Kingdom
maskPhone('+44 20 7123 4567', { preserveFormat: true });
// Output: '+** ** **** 4567'
// Indonesia
maskPhone('+62 812 3456 7890', { preserveFormat: true });
// Output: '+** *** **** 7890'
// France
maskPhone('+33 1 23 45 67 89', { preserveFormat: true });
// Output: '+** * ** ** 67 89'
// Japan
maskPhone('+81 3-1234-5678', { preserveFormat: true });
// Output: '+** *-****-5678'
// Australia
maskPhone('+61 2 1234 5678', { preserveFormat: true });
// Output: '+** * **** 5678'
// Germany
maskPhone('+49 30 12345678', { preserveFormat: true });
// Output: '+** ** ****5678'
// Brazil
maskPhone('+55 11 91234-5678', { preserveFormat: true });
// Output: '+** ** *****-5678'
// China
maskPhone('+86 138 0013 8000', { preserveFormat: true });
// Output: '+** *** **** 8000'
// Russia
maskPhone('+7 495 123-45-67', { preserveFormat: true });
// Output: '+* *** ***-**-67'This library helps comply with GDPR (General Data Protection Regulation) requirements for handling personal data:
π GDPR Article 32 - Security of Processing
- Pseudonymization and masking of personal data
- Phone number masking is a technical measure for privacy protection
Recommended practices:
// β
GDPR Compliant (Last 4 only - Default)
maskPhone('+1234567890');
// Output: '+******7890'
// β
GDPR Compliant (Minimal disclosure)
maskPhone('+1234567890', { showLast: 2 });
// Output: '+*********90'
// β οΈ Use with caution (more data disclosed)
maskPhone('+1234567890', { showFirst: 3, showLast: 4 });
// Output: '+12****7890'Following OWASP and NIST SP 800-122 guidelines:
- Minimize data exposure - Show only last 4 digits (default)
- Never log unmasked phone numbers in production systems
- Use masking for display purposes - Don't use for authentication
- This library is for display only - Not for validation or storage
- Backend compliance - Ensure server properly handles PII
π This library is designed for display and logging purposes. It does not:
- Store phone numbers securely
- Validate phone number format or authenticity
- Hash or encrypt phone data
- Handle actual telecommunications
- Provide country/carrier detection
This is a pure masking utility that works in both frontend and backend environments (Node.js, browser, serverless functions, etc.).
Always ensure your systems comply with GDPR, CCPA, and local data protection regulations when handling personal information.
The library handles various edge cases gracefully:
// Very short numbers (won't mask if length β€ showLast)
maskPhone('1234');
// Output: '1234'
maskPhone('12345');
// Output: '*2345'
// Empty input
maskPhone('');
// Output: ''
// Null/undefined
maskPhone(null);
// Output: ''
// Whitespace only
maskPhone(' ');
// Output: ''
// Mixed characters (auto-strips non-digits except +)
maskPhone('+1-ABC-234-5678');
// Output: '+******5678'
// Multiple + signs (keeps only digits and first +)
maskPhone('++1234567890');
// Output: '+******7890'
// Only + sign
maskPhone('+');
// Output: '+'
// Very long numbers
maskPhone('123456789012345678901234567890');
// Output: '**************************7890'- β‘ Lightweight: < 2KB minified + gzipped
- π Zero dependencies
- π¨ Fast execution (< 1ms for typical phones)
- π³ Tree-shakeable with
sideEffects: false - π¦ Supports both CJS and ESM
This library works in all modern browsers and Node.js environments that support ES2020+.
- β Chrome (latest)
- β Firefox (latest)
- β Safari (latest)
- β Edge (latest)
- β Node.js 14+
- β Deno
- β Bun
All types and utilities are exported for TypeScript users:
import {
maskPhone, // Main function
type MaskOptions, // Options interface
type PhoneInput, // Input type (string | number)
DEFAULT_OPTIONS, // Default configuration constants
isValidPhoneInput // Type guard utility
} from '@ekaone/mask-phone';
// Using the type guard
function processPhone(input: unknown) {
if (isValidPhoneInput(input)) {
return maskPhone(input);
}
throw new Error('Invalid phone input');
}
// Using DEFAULT_OPTIONS
const customOptions: MaskOptions = {
...DEFAULT_OPTIONS,
showFirst: 3,
};
// Strongly typed options
const options: MaskOptions = {
maskChar: 'β’',
showLast: 4,
preserveFormat: true,
visibleRanges: [[0, 2], [8, 10]],
customMask: (char, idx, phone) => char === '5' ? 'X' : char
};
// Type-safe function wrapper
function safeMaskPhone(phone: PhoneInput, options?: MaskOptions): string {
return maskPhone(phone, options);
}Full TypeScript support with comprehensive type definitions included.
import {
maskPhone,
type MaskOptions,
type PhoneInput,
DEFAULT_OPTIONS,
isValidPhoneInput
} from '@ekaone/mask-phone';
// Basic usage with types
const phone: PhoneInput = '+1234567890';
const options: MaskOptions = {
maskChar: 'β’',
showLast: 4,
preserveFormat: true
};
const masked: string = maskPhone(phone, options);
// Using type guard for runtime validation
function handleUserInput(input: unknown): string {
if (!isValidPhoneInput(input)) {
throw new Error('Invalid phone number format');
}
return maskPhone(input);
}
// Extending options with custom configuration
interface MyAppPhoneOptions extends MaskOptions {
logAccess?: boolean;
}
function maskWithLogging(
phone: PhoneInput,
options: MyAppPhoneOptions
): string {
const masked = maskPhone(phone, options);
if (options.logAccess) {
console.log('Phone accessed:', masked);
}
return masked;
}
// Type-safe custom masking function
const customMask: MaskOptions['customMask'] = (
char: string,
index: number,
phone: string
): string => {
return index % 2 === 0 ? '*' : char;
};
maskPhone('1234567890', { customMask });The package provides full IntelliSense support in various IDEs and other TypeScript-aware editors:
- π‘ Auto-completion for all options
- π Inline documentation with examples
- π Type checking for function parameters
β οΈ Compile-time error detection
Unlike other phone masking libraries, we make zero assumptions about phone number format. Works with any country, any format, any length.
Keep your original formatting with preserveFormat: true - perfect for UI display.
From simple last-4 masking to complex custom functions - you're in control.
Follows GDPR, OWASP, and NIST guidelines for PII protection.
- Full TypeScript support
- Intuitive API
- Comprehensive documentation
- Extensive test coverage
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
# Install dependencies
npm install
# Run tests
npm test
# Watch mode
npm run test:watch
# Coverage
npm run test:coverage
# Build
npm run build
# Type check
npm run type-checkMIT Β© Eka Prasetia
- @ekaone/mask-card - Credit card masking library
- @ekaone/mask-email - Email address masking library
β If this library helps you, please consider giving it a star on GitHub!