Utilities Package (@schwab/utilities)
Overview
The @schwab/utilities package provides a comprehensive collection of utility functions, logging capabilities, feature flag management, and common helper functions that are shared across all applications and packages in the Charles Schwab monorepo. It serves as the foundational utility layer for consistent data processing, validation, and system operations.
Architecture
Core Function Categories
1. String Manipulation Utilities
String Processing Functions
// camelize.ts - Convert strings to camelCase format
export const camelize = (str: string, capitalizeFirstLetter: boolean): string => {
let newStr = str
?.toLowerCase()
.replace(/[^a-zA-Z0-9]+(?<character>.)/g, (_m, chr: string) => chr.toUpperCase());
if (capitalizeFirstLetter) {
newStr = newStr.substring(0, 1).toUpperCase() + newStr.substring(1);
}
return newStr;
};
// Alternative camelization for hyphenated strings
export const camelizeSplit = (str: string): string =>
str.replace(/(?<dashes>^|-)(?<whitespaces>\w)/g, (_, p1: string, p2: string) =>
p1 === '-' ? p2.toUpperCase() : p2.toUpperCase(),
);
// convertToPascal.ts - Convert to PascalCase
export function convertToPascal(str: string): string {
return str
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) =>
index === 0 ? word.toLowerCase() : word.toUpperCase()
)
.replace(/\s+/g, '');
}
// toKebabCase.ts - Convert to kebab-case
export function toKebabCase(str: string): string {
return str
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/[\s_]+/g, '-')
.toLowerCase();
}
// toTitleCase.ts - Convert to Title Case
export function toTitleCase(str: string): string {
return str
.toLowerCase()
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
// removePercent.ts - Remove percentage signs and convert to number
export function removePercent(value: string): number {
return parseFloat(value.replace('%', '')) / 100;
}
// Usage Examples
const examples = {
camelCase: camelize('hello-world-test', false), // 'helloWorldTest'
pascalCase: convertToPascal('hello world'), // 'HelloWorld'
kebabCase: toKebabCase('HelloWorldTest'), // 'hello-world-test'
titleCase: toTitleCase('hello world test'), // 'Hello World Test'
percentConversion: removePercent('75.5%'), // 0.755
};
2. Data Validation and Type Guards
Validation and Type Guard Functions
// isArrayWithData.ts - Check if value is array with data
export function isArrayWithData<T>(value: unknown): value is T[] {
return Array.isArray(value) && value.length > 0;
}
// isBooleanTrue.ts & isBooleanFalse.ts - Boolean validation
export function isBooleanTrue(value: unknown): value is true {
return value === true || value === 'true' || value === 1 || value === '1';
}
export function isBooleanFalse(value: unknown): value is false {
return value === false || value === 'false' || value === 0 || value === '0';
}
// parseBoolean.ts - Parse various boolean representations
export function parseBoolean(value: unknown): boolean {
if (typeof value === 'boolean') return value;
if (typeof value === 'string') {
const lowered = value.toLowerCase();
return lowered === 'true' || lowered === '1' || lowered === 'yes';
}
if (typeof value === 'number') {
return value !== 0;
}
return false;
}
// isObjectLiteral.ts - Check if value is plain object
export function isObjectLiteral(value: unknown): value is Record<string, unknown> {
return (
typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
Object.prototype.toString.call(value) === '[object Object]'
);
}
// areValuesSet.ts - Check if all values in object are set
export function areValuesSet<T extends Record<string, unknown>>(
obj: T,
requiredKeys?: (keyof T)[]
): boolean {
const keysToCheck = requiredKeys || Object.keys(obj);
return keysToCheck.every(key => {
const value = obj[key];
return value !== null && value !== undefined && value !== '';
});
}
// doesObjectHaveKeys.ts - Validate object has required keys
export function doesObjectHaveKeys<T extends Record<string, unknown>>(
obj: T,
requiredKeys: (keyof T)[]
): boolean {
return requiredKeys.every(key => key in obj);
}
// Usage Examples
const validationExamples = {
arrayCheck: isArrayWithData([1, 2, 3]), // true
arrayEmpty: isArrayWithData([]), // false
booleanCheck: parseBoolean('yes'), // true
booleanString: parseBoolean('false'), // false
objectValidation: isObjectLiteral({ key: 'value' }), // true
objectArray: isObjectLiteral([]), // false
requiredFields: areValuesSet({ name: 'John', age: 30 }), // true
missingFields: areValuesSet({ name: '', age: null }), // false
};
3. Array and Object Processing
Array and Object Utilities
// getArrayIndex.ts - Safe array index access
export function getArrayIndex<T>(
array: T[],
index: number,
defaultValue?: T
): T | undefined {
if (!isArrayWithData(array) || index < 0 || index >= array.length) {
return defaultValue;
}
return array[index];
}
// dedupeList.ts - Remove duplicates from array
export function dedupeList<T>(
array: T[],
keyFn?: (item: T) => string | number
): T[] {
if (!keyFn) {
return [...new Set(array)];
}
const seen = new Set<string | number>();
return array.filter(item => {
const key = keyFn(item);
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
// doAllObjectsHaveKeys.ts - Validate array of objects
export function doAllObjectsHaveKeys<T extends Record<string, unknown>>(
objects: T[],
requiredKeys: (keyof T)[]
): boolean {
return objects.every(obj => doesObjectHaveKeys(obj, requiredKeys));
}
// doSomeObjectsHaveKeys.ts - Check if some objects have keys
export function doSomeObjectsHaveKeys<T extends Record<string, unknown>>(
objects: T[],
requiredKeys: (keyof T)[]
): boolean {
return objects.some(obj => doesObjectHaveKeys(obj, requiredKeys));
}
// Usage Examples
const arrayUtilExamples = {
safeAccess: getArrayIndex(['a', 'b', 'c'], 1), // 'b'
outOfBounds: getArrayIndex(['a'], 5, 'default'), // 'default'
dedupePrimitives: dedupeList([1, 2, 2, 3, 1]), // [1, 2, 3]
dedupeObjects: dedupeList(
[{ id: 1, name: 'A' }, { id: 2, name: 'B' }, { id: 1, name: 'C' }],
item => item.id
), // [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]
allObjectsValid: doAllObjectsHaveKeys(
[{ name: 'A', age: 1 }, { name: 'B', age: 2 }],
['name', 'age']
), // true
};
4. Rich Text and HTML Processing
Rich Text and HTML Utilities
// sanitizeRichText.ts - Secure HTML sanitization
import sanitizeHtml from 'sanitize-html';
export function sanitizeRichText(text: string): string {
return sanitizeHtml(text, {
allowedTags: ['img', ...sanitizeHtml.defaults.allowedTags],
allowedClasses: {
// Allows all classes on all tags
'*': false,
},
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
'img': ['src', 'alt', 'title', 'width', 'height', 'loading'],
'a': ['href', 'title', 'target', 'rel'],
},
allowedSchemes: ['http', 'https', 'mailto', 'tel'],
});
}
// stripHTML.ts - Remove all HTML tags
export function stripHTML(html: string): string {
return html.replace(/<[^>]*>/g, '').trim();
}
// isRichText.ts - Check if content contains HTML
export function isRichText(content: string): boolean {
const htmlPattern = /<\/?[a-z][\s\S]*>/i;
return htmlPattern.test(content);
}
// tokenizedText.ts - Process tokenized content
export function tokenizedText(
template: string,
tokens: Record<string, string>
): string {
return template.replace(/\{\{(\w+)\}\}/g, (match, token) => {
return tokens[token] || match;
});
}
// Usage Examples
const textProcessingExamples = {
sanitized: sanitizeRichText('<script>alert("xss")</script><p>Safe content</p>'),
// Result: '<p>Safe content</p>'
stripped: stripHTML('<p>Hello <strong>world</strong>!</p>'),
// Result: 'Hello world!'
richTextCheck: isRichText('<p>Rich content</p>'), // true
plainTextCheck: isRichText('Plain content'), // false
tokenReplacement: tokenizedText(
'Hello {{name}}, your balance is {{balance}}',
{ name: 'John', balance: '$1,000' }
),
// Result: 'Hello John, your balance is $1,000'
};
URL and Format Processing
1. URL and Slug Utilities
URL Processing Functions
// parseUrl.ts - Robust URL parsing
export interface ParsedUrl {
protocol?: string;
hostname?: string;
pathname: string;
search?: string;
hash?: string;
isValid: boolean;
}
export function parseUrl(url: string): ParsedUrl {
try {
const parsed = new URL(url, 'https://example.com');
return {
protocol: parsed.protocol,
hostname: parsed.hostname,
pathname: parsed.pathname,
search: parsed.search,
hash: parsed.hash,
isValid: true,
};
} catch {
return {
pathname: url.startsWith('/') ? url : `/${url}`,
isValid: false,
};
}
}
// format-url-slug.ts - Create URL-safe slugs
export function formatUrlSlug(text: string): string {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, '') // Remove special characters
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-') // Remove duplicate hyphens
.trim();
}
// isValidQueryParam.ts - Validate query parameters
export function isValidQueryParam(param: unknown): param is string {
return typeof param === 'string' && param.length > 0 && param !== 'undefined';
}
// Usage Examples
const urlExamples = {
parsedUrl: parseUrl('https://schwab.com/trading?type=stocks#section1'),
// Result: { protocol: 'https:', hostname: 'schwab.com', pathname: '/trading', ... }
slug: formatUrlSlug('Investment Options & Planning'), // 'investment-options-planning'
validParam: isValidQueryParam('stocks'), // true
invalidParam: isValidQueryParam(''), // false
};
2. Date and Number Formatting
Date and Number Utilities
// dateFormat.ts - Date formatting utilities
export interface DateFormatOptions {
locale?: string;
timeZone?: string;
dateStyle?: 'full' | 'long' | 'medium' | 'short';
timeStyle?: 'full' | 'long' | 'medium' | 'short';
}
export function dateFormat(
date: Date | string | number,
options: DateFormatOptions = {}
): string {
const dateObj = new Date(date);
if (isNaN(dateObj.getTime())) {
throw new Error('Invalid date provided');
}
const defaultOptions: Intl.DateTimeFormatOptions = {
locale: 'en-US',
timeZone: 'America/New_York',
...options,
};
return new Intl.DateTimeFormat(defaultOptions.locale, defaultOptions).format(dateObj);
}
// getAspectRatio.ts - Calculate aspect ratios
export function getAspectRatio(width: number, height: number): string {
const gcd = (a: number, b: number): number => b === 0 ? a : gcd(b, a % b);
const divisor = gcd(width, height);
return `${width / divisor}:${height / divisor}`;
}
// Usage Examples
const formatExamples = {
dateFormatted: dateFormat(new Date(), { dateStyle: 'medium' }),
// Result: 'Jan 15, 2024'
aspectRatio: getAspectRatio(1920, 1080), // '16:9'
aspectRatioSquare: getAspectRatio(500, 500), // '1:1'
};
Logging System
1. SchLogger - Environment-Aware Logging
Logging System (schLogger.ts)
export const SchLogger = (() => {
const getLoggerLevel = () => {
const LoggerLevel = `${process.env.LOGGER_LEVEL}`;
let level = 3;
switch (LoggerLevel) {
case 'DEFAULT':
level = 0; // All logs
break;
case 'INFO':
level = 1; // Info and above
break;
case 'WARN':
level = 2; // Warn and above
break;
case 'ERROR':
level = 3; // Error only
break;
default:
level = 3;
}
return level;
};
const loggerLevel = getLoggerLevel();
const log = (...messages: unknown[]) => {
if (loggerLevel === 0) {
console.log(...messages);
}
};
const info = (...messages: unknown[]) => {
if (loggerLevel <= 1) {
console.info(...messages);
}
};
const warn = (...messages: unknown[]) => {
if (loggerLevel <= 2) {
console.warn(...messages);
}
};
const error = (...messages: unknown[]) => {
if (loggerLevel <= 3) {
console.error(...messages);
}
};
const data = (...messages: unknown[]) => {
if (loggerLevel <= 3) {
console.log('DATA:', ...messages);
}
};
return {
log,
info,
warn,
error,
data,
};
})();
// Usage Examples
SchLogger.info('User authentication successful', { userId: 123 });
SchLogger.warn('Rate limit approaching', { remainingRequests: 5 });
SchLogger.error('Database connection failed', { error: 'Connection timeout' });
SchLogger.data('Analytics event', { event: 'page_view', path: '/trading' });
Feature Flag Management
1. LaunchDarkly Integration
Feature Flags System (flags/index.ts)
import { createLaunchDarklyAdapter, type LDContext } from '@flags-sdk/launchdarkly';
import type { FlagDeclaration, ReadonlyHeaders, ReadonlyRequestCookies } from 'flags';
import { dedupe, Flag, flag, generatePermutations } from 'flags/next';
type Flags = Parameters<typeof generatePermutations>[0];
const adapter = createLaunchDarklyAdapter({
projectSlug: process.env.LAUNCHDARKLY_PROJECT_KEY ?? '',
clientSideId: process.env.LAUNCHDARKLY_CLIENTSIDE_ID ?? '',
edgeConfigConnectionString: process.env.EDGE_CONFIG_FLAGS ?? '',
});
/**
* Loads all flags files and returns them as an array
*/
export const requireAll = (ctx) => {
return (
ctx
.keys()
.map(ctx)
.map((m) => m.default)
.filter(Boolean)
);
};
/**
* Create a typed flag declaration
*/
export function createFlag<T = boolean>(
key: string,
defaultValue: T,
description?: string
): FlagDeclaration<T> {
return {
key,
defaultValue,
description,
options: {
description,
},
};
}
/**
* Get flag value with context
*/
export async function getFlagValue<T>(
flagKey: string,
context: LDContext,
defaultValue: T
): Promise<T> {
try {
return await adapter.getFlag(flagKey, context, defaultValue);
} catch (error) {
SchLogger.warn(`Failed to get flag ${flagKey}:`, error);
return defaultValue;
}
}
/**
* Batch get multiple flags
*/
export async function getFlags(
flagKeys: string[],
context: LDContext
): Promise<Record<string, unknown>> {
const results: Record<string, unknown> = {};
await Promise.allSettled(
flagKeys.map(async (key) => {
try {
results[key] = await adapter.getFlag(key, context);
} catch (error) {
SchLogger.warn(`Failed to get flag ${key}:`, error);
results[key] = false; // Default fallback
}
})
);
return results;
}
// Usage Examples
const flagExamples = {
// Flag definition
newTradingInterface: createFlag(
'new-trading-interface',
false,
'Enable new trading interface redesign'
),
// Getting flag value
async checkFeature() {
const context = { key: 'user-123', email: 'user@schwab.com' };
const isEnabled = await getFlagValue('new-trading-interface', context, false);
return isEnabled;
},
// Batch flag retrieval
async getMultipleFlags() {
const context = { key: 'user-123' };
const flags = await getFlags([
'new-trading-interface',
'enhanced-analytics',
'mobile-chat-support'
], context);
return flags;
}
};
Advanced Utilities
1. Response and Key Generation
Response and Key Utilities
// responseCreate.ts - Standardized API response creation
export interface StandardResponse<T = unknown> {
success: boolean;
data?: T;
error?: string;
message?: string;
timestamp: string;
requestId?: string;
}
export function responseCreate<T>(
success: boolean,
data?: T,
error?: string,
message?: string
): StandardResponse<T> {
return {
success,
data,
error,
message,
timestamp: new Date().toISOString(),
requestId: generateRequestId(),
};
}
// keyCreate.ts - Generate unique keys
import { nanoid } from 'nanoid';
export function keyCreate(prefix?: string, length: number = 10): string {
const id = nanoid(length);
return prefix ? `${prefix}_${id}` : id;
}
export function generateRequestId(): string {
return keyCreate('req', 12);
}
// runtimeError.ts - Runtime error handling
export class RuntimeError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500,
public metadata?: Record<string, unknown>
) {
super(message);
this.name = 'RuntimeError';
}
}
export function createRuntimeError(
message: string,
code: string,
statusCode?: number,
metadata?: Record<string, unknown>
): RuntimeError {
return new RuntimeError(message, code, statusCode, metadata);
}
// Usage Examples
const advancedUtilExamples = {
successResponse: responseCreate(true, { user: 'John' }, undefined, 'User created'),
errorResponse: responseCreate(false, undefined, 'Validation failed', 'Invalid input'),
uniqueKey: keyCreate('user'), // 'user_Kj8sF3mN2p'
requestId: generateRequestId(), // 'req_A7sK9mP3qR8t'
customError: createRuntimeError(
'Database connection failed',
'DB_CONNECTION_ERROR',
503,
{ database: 'users', attempts: 3 }
),
};
2. Testing and Development Utilities
Testing and Development Support
// transformZodToArgTypes.ts - Storybook integration
import type { z } from 'zod';
export function transformZodToArgTypes(schema: z.ZodSchema): Record<string, unknown> {
// Transform Zod schema to Storybook arg types
const argTypes: Record<string, unknown> = {};
if (schema instanceof z.ZodObject) {
const shape = schema.shape;
Object.keys(shape).forEach(key => {
const fieldSchema = shape[key];
argTypes[key] = getArgTypeFromZod(fieldSchema);
});
}
return argTypes;
}
function getArgTypeFromZod(schema: z.ZodSchema): Record<string, unknown> {
if (schema instanceof z.ZodString) {
return { control: 'text' };
} else if (schema instanceof z.ZodNumber) {
return { control: 'number' };
} else if (schema instanceof z.ZodBoolean) {
return { control: 'boolean' };
} else if (schema instanceof z.ZodEnum) {
return {
control: 'select',
options: schema.options,
};
}
return { control: 'object' };
}
// get-site-context.ts - Environment context utilities
export interface SiteContext {
environment: 'development' | 'staging' | 'production';
version: string;
buildTime: string;
region: string;
}
export function getSiteContext(): SiteContext {
return {
environment: (process.env.NODE_ENV as any) || 'development',
version: process.env.APP_VERSION || '0.0.0',
buildTime: process.env.BUILD_TIME || new Date().toISOString(),
region: process.env.REGION || 'us-east-1',
};
}
// getAppVersion.ts - Application versioning
export function getAppVersion(): string {
return process.env.APP_VERSION ||
process.env.npm_package_version ||
'0.0.0';
}
// Usage Examples
const devUtilExamples = {
// Storybook integration
argTypes: transformZodToArgTypes(z.object({
name: z.string(),
age: z.number(),
active: z.boolean(),
role: z.enum(['admin', 'user', 'guest'])
})),
context: getSiteContext(),
version: getAppVersion(),
};
SFMC Integration Utilities
1. Salesforce Marketing Cloud Utils
SFMC Utilities (sfmc/sfmc-utils.ts)
// SFMC integration utilities for marketing automation
export interface SfmcContactData {
email: string;
firstName?: string;
lastName?: string;
phone?: string;
preferences?: Record<string, boolean>;
customAttributes?: Record<string, unknown>;
}
export interface SfmcEventData {
eventType: string;
contactKey: string;
eventDate: string;
properties: Record<string, unknown>;
}
export class SfmcUtils {
/**
* Validate SFMC contact data structure
*/
static validateContactData(data: unknown): data is SfmcContactData {
if (!isObjectLiteral(data)) return false;
const contact = data as Record<string, unknown>;
// Email is required
if (typeof contact.email !== 'string' || !contact.email.includes('@')) {
return false;
}
// Optional fields validation
if (contact.firstName && typeof contact.firstName !== 'string') {
return false;
}
if (contact.lastName && typeof contact.lastName !== 'string') {
return false;
}
return true;
}
/**
* Sanitize contact data for SFMC submission
*/
static sanitizeContactData(data: SfmcContactData): SfmcContactData {
return {
email: data.email.toLowerCase().trim(),
firstName: data.firstName?.trim(),
lastName: data.lastName?.trim(),
phone: data.phone?.replace(/\D/g, ''), // Remove non-digits
preferences: data.preferences || {},
customAttributes: data.customAttributes || {},
};
}
/**
* Create SFMC event payload
*/
static createEventPayload(
eventType: string,
contactKey: string,
properties: Record<string, unknown>
): SfmcEventData {
return {
eventType,
contactKey,
eventDate: new Date().toISOString(),
properties: {
...properties,
timestamp: Date.now(),
source: 'website',
},
};
}
/**
* Generate contact key from email
*/
static generateContactKey(email: string): string {
return `contact_${btoa(email.toLowerCase()).replace(/[^a-zA-Z0-9]/g, '')}`;
}
}
// Usage Examples
const sfmcExamples = {
// Validate contact data
validContact: SfmcUtils.validateContactData({
email: 'user@schwab.com',
firstName: 'John',
lastName: 'Doe'
}), // true
// Sanitize data
sanitizedContact: SfmcUtils.sanitizeContactData({
email: ' USER@SCHWAB.COM ',
firstName: 'John ',
phone: '(555) 123-4567'
}),
// Result: { email: 'user@schwab.com', firstName: 'John', phone: '5551234567' }
// Create event
eventPayload: SfmcUtils.createEventPayload(
'page_view',
'contact_12345',
{ page: '/trading', section: 'stocks' }
),
// Generate contact key
contactKey: SfmcUtils.generateContactKey('user@schwab.com'),
};
Best Practices and Usage Patterns
1. Error Handling Patterns
Recommended Error Handling
import { SchLogger } from '@schwab/utilities/SchLogger';
import { createRuntimeError } from '@schwab/utilities/functions/runtimeError';
// Example of comprehensive error handling
export async function processUserData(userData: unknown): Promise<StandardResponse> {
try {
// Validate input
if (!isObjectLiteral(userData)) {
throw createRuntimeError(
'Invalid user data format',
'INVALID_INPUT',
400,
{ receivedType: typeof userData }
);
}
// Process data with validation
const requiredFields = ['email', 'firstName'];
if (!doesObjectHaveKeys(userData, requiredFields)) {
throw createRuntimeError(
'Missing required fields',
'MISSING_FIELDS',
400,
{ required: requiredFields, received: Object.keys(userData) }
);
}
// Sanitize and process
const sanitizedData = {
email: (userData.email as string).toLowerCase().trim(),
firstName: sanitizeRichText(userData.firstName as string),
};
SchLogger.info('User data processed successfully', { email: sanitizedData.email });
return responseCreate(true, sanitizedData, undefined, 'Data processed successfully');
} catch (error) {
if (error instanceof RuntimeError) {
SchLogger.warn('User data processing failed:', error.message, error.metadata);
return responseCreate(false, undefined, error.message);
}
SchLogger.error('Unexpected error processing user data:', error);
return responseCreate(false, undefined, 'Internal server error');
}
}
2. Performance Optimization
Performance Best Practices
// Memoization for expensive calculations
const memoCache = new Map<string, unknown>();
export function memoize<T extends (...args: any[]) => any>(
fn: T,
keyGenerator?: (...args: Parameters<T>) => string
): T {
return ((...args: Parameters<T>) => {
const key = keyGenerator ? keyGenerator(...args) : JSON.stringify(args);
if (memoCache.has(key)) {
return memoCache.get(key);
}
const result = fn(...args);
memoCache.set(key, result);
return result;
}) as T;
}
// Batch processing utilities
export async function batchProcess<T, U>(
items: T[],
processor: (item: T) => Promise<U>,
batchSize: number = 10
): Promise<U[]> {
const results: U[] = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(item => processor(item))
);
results.push(...batchResults);
}
return results;
}
// Usage
const memoizedFormatUrl = memoize(formatUrlSlug);
const processedData = await batchProcess(userList, processUserData);
The Utilities package serves as the foundational utility layer for the Charles Schwab monorepo, providing consistent, well-tested, and performant helper functions that ensure code quality, security, and maintainability across all applications and services.