Utilities for working with arrays, objects, and type-safe data access.
This module provides the following functionality:
/**
* Check if two arrays have any common elements
* @param tags - First array of strings
* @param tags2 - Second array of strings
* @returns true if arrays share at least one element
*/
function hasIntersection(tags: string[], tags2: string[]): boolean;
/**
* Toggle the presence of a value in an array
* Adds the value if not present, removes it if present
* @param initialArray - The array to modify
* @param value - The value to toggle
* @returns New array with value toggled
*/
function toggleArrayValue<T = string>(initialArray: T[], value: T): T[];
/**
* Replace all exact matches of a string in an array
* Note: This replaces entire array elements that exactly match, not substrings within elements
* @param arrayToUpdate - Array containing strings to update
* @param valueToReplace - String value to find and replace (must match exactly)
* @param newValue - Replacement string
* @returns New array with exact matches replaced
*/
function replaceStringInArray(
arrayToUpdate: string[],
valueToReplace: string,
newValue: string
): string[];Examples:
import {
hasIntersection,
toggleArrayValue,
replaceStringInArray,
} from '@lightdash/common';
// Check for common tags
const userTags = ['analytics', 'bi', 'reporting'];
const requiredTags = ['bi', 'dashboard'];
if (hasIntersection(userTags, requiredTags)) {
console.log('User has required tags');
}
// Toggle dimension in selection
let selectedDimensions = ['customer_id', 'customer_name'];
selectedDimensions = toggleArrayValue(selectedDimensions, 'customer_email');
// Result: ['customer_id', 'customer_name', 'customer_email']
selectedDimensions = toggleArrayValue(selectedDimensions, 'customer_name');
// Result: ['customer_id', 'customer_email'] (removed)
// Replace exact string matches in array
let tags = ['analytics', 'bi', 'analytics', 'reporting'];
tags = replaceStringInArray(tags, 'analytics', 'data-analytics');
// Result: ['data-analytics', 'bi', 'data-analytics', 'reporting']
// Note: For substring replacement within elements, use Array.map with String.replace:
let fieldIds = ['old_table.field1', 'old_table.field2', 'other_table.field3'];
fieldIds = fieldIds.map(id => id.replace('old_table', 'new_table'));
// Result: ['new_table.field1', 'new_table.field2', 'other_table.field3']/**
* Type guard to filter out null values from arrays
* @param arg - Value to check for null
* @returns true if the value is not null, with TypeScript type narrowing
*/
function isNotNull<T>(arg: T): arg is Exclude<T, null>;Example:
import { isNotNull } from '@lightdash/common';
// Filter null values from array
const values: (string | null)[] = ['hello', null, 'world', null, 'foo'];
const nonNullValues = values.filter(isNotNull);
// Type: string[]
// Value: ['hello', 'world', 'foo']
// Use with field IDs
const fieldIds: (FieldId | null)[] = [
'customers.customer_id',
null,
'orders.order_date',
null,
];
const validFieldIds = fieldIds.filter(isNotNull);
// Type: FieldId[]
// Value: ['customers.customer_id', 'orders.order_date']
// Works with complex types
interface User {
id: string;
name: string;
}
const users: (User | null)[] = [
{ id: '1', name: 'Alice' },
null,
{ id: '2', name: 'Bob' },
];
const validUsers = users.filter(isNotNull);
// Type: User[]
// Value: [{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }]Functions for accessing nested data structures. These functions throw UnexpectedIndexError if the value is not found.
/**
* Gets a value from an array by index, throws if not found
* @param obj - Array or array-like object
* @param key - Index to access
* @param errorMessage - Optional custom error message
* @returns The value at the index
* @throws UnexpectedIndexError if value not found
*/
function getArrayValue<T>(
obj: ArrayLike<T> | undefined,
key: number,
errorMessage?: string
): T;
/**
* Gets a value from an object by key, throws if not found
* @param obj - Object to access
* @param key - Key to access (string or number)
* @param errorMessage - Optional custom error message
* @returns The value at the key
* @throws UnexpectedIndexError if value not found
*/
function getObjectValue<T>(
obj: Record<string | number, T> | undefined,
key: string | number,
errorMessage?: string
): T;
/**
* Type-safe property checking
* @param obj - Object to check
* @param key - Property key to check
* @returns true if property exists, with type narrowing
*/
function hasProperty<T>(
obj: unknown,
key: string
): obj is Record<string, T>;import { getArrayValue } from '@lightdash/common';
// Array access - throws if out of bounds
const items = ['a', 'b', 'c'];
const second = getArrayValue(items, 1); // 'b'
try {
const missing = getArrayValue(items, 10); // Throws UnexpectedIndexError
} catch (error) {
console.error('Index not found');
}
// With custom error message
const colors = ['red', 'green', 'blue'];
try {
const color = getArrayValue(colors, 5, 'Color index out of range');
} catch (error) {
console.error(error.message); // "Color index out of range"
}
// With results from query
const queryResults = [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 },
];
const firstResult = getArrayValue(queryResults, 0);
console.log(firstResult.name); // "Alice"import { getObjectValue } from '@lightdash/common';
// Object access - throws if key not found
const config = { theme: 'dark', size: 'large' };
const theme = getObjectValue(config, 'theme'); // 'dark'
try {
const missing = getObjectValue(config, 'color'); // Throws UnexpectedIndexError
} catch (error) {
console.error('Key not found');
}
// With custom error message
const settings = { language: 'en', timezone: 'UTC' };
try {
const currency = getObjectValue(
settings,
'currency',
'Currency setting is required'
);
} catch (error) {
console.error(error.message); // "Currency setting is required"
}
// With field map
const fieldMap = {
'customers.id': { type: 'number', label: 'Customer ID' },
'customers.name': { type: 'string', label: 'Customer Name' },
};
const customerIdField = getObjectValue(fieldMap, 'customers.id');
console.log(customerIdField.label); // "Customer ID"import { hasProperty } from '@lightdash/common';
// Type-safe property checking
const data: unknown = { name: 'John', age: 30 };
if (hasProperty<string>(data, 'name')) {
// TypeScript now knows data is Record<string, string>
console.log(data.name.toUpperCase());
}
if (hasProperty<number>(data, 'age')) {
console.log(data.age + 1);
}
// Safe property access on unknown objects
function processData(obj: unknown) {
if (hasProperty<string>(obj, 'id') && hasProperty<string>(obj, 'name')) {
return {
id: obj.id,
name: obj.name,
};
}
throw new Error('Invalid data structure');
}import { getArrayValue, getObjectValue } from '@lightdash/common';
function processQueryResults(results: QueryResults) {
// Get first row (throws if empty)
const firstRow = getArrayValue(results.rows, 0, 'No results found');
// Get specific field value (throws if field doesn't exist)
const customerId = getObjectValue(
firstRow,
'customers.id',
'Customer ID field not found in results'
);
return customerId;
}
// Usage
try {
const id = processQueryResults(queryResults);
console.log('Customer ID:', id);
} catch (error) {
console.error('Failed to process results:', error.message);
}import { getObjectValue, hasProperty } from '@lightdash/common';
function getFieldValue<T>(
row: Record<string, unknown>,
fieldId: string
): T {
if (!hasProperty<T>(row, fieldId)) {
throw new Error(`Field '${fieldId}' not found in row`);
}
return getObjectValue<T>(row, fieldId);
}
// Usage
const row = {
'customers.name': 'Alice',
'orders.total': 150.50,
};
const customerName = getFieldValue<string>(row, 'customers.name');
const orderTotal = getFieldValue<number>(row, 'orders.total');import { getArrayValue, isNotNull } from '@lightdash/common';
function processBatch<T>(
items: (T | null)[],
processor: (item: T) => void
) {
// Filter out null items
const validItems = items.filter(isNotNull);
// Process each item safely
for (let i = 0; i < validItems.length; i++) {
try {
const item = getArrayValue(validItems, i);
processor(item);
} catch (error) {
console.error(`Failed to process item ${i}:`, error.message);
}
}
}
// Usage
const items: (Product | null)[] = [
{ id: '1', name: 'Widget' },
null,
{ id: '2', name: 'Gadget' },
];
processBatch(items, (product) => {
console.log(`Processing ${product.name}`);
});import { hasProperty, getObjectValue } from '@lightdash/common';
interface ValidatedData {
id: string;
name: string;
email: string;
}
function validateAndExtract(data: unknown): ValidatedData {
// Check required properties
if (
!hasProperty<string>(data, 'id') ||
!hasProperty<string>(data, 'name') ||
!hasProperty<string>(data, 'email')
) {
throw new Error('Missing required fields');
}
// Extract with type safety
return {
id: getObjectValue(data, 'id'),
name: getObjectValue(data, 'name'),
email: getObjectValue(data, 'email'),
};
}
// Usage
try {
const validated = validateAndExtract(userInput);
console.log('Valid data:', validated);
} catch (error) {
console.error('Validation failed:', error.message);
}import { getArrayValue, getObjectValue, isNotNull } from '@lightdash/common';
function prepareChartData(
rows: ResultRow[],
xField: string,
yFields: string[]
) {
// Filter null rows
const validRows = rows.filter(isNotNull);
return validRows.map((row, index) => {
try {
// Get x-axis value
const x = getObjectValue(row, xField);
// Get y-axis values
const y = yFields.map(field => getObjectValue(row, field));
return { x, y };
} catch (error) {
throw new Error(
`Failed to process row ${index}: ${error.message}`
);
}
});
}import {
isNotNull,
getArrayValue,
getObjectValue,
hasProperty,
} from '@lightdash/common';
describe('Array utilities', () => {
describe('isNotNull', () => {
it('should filter null values', () => {
const arr = [1, null, 2, null, 3];
const result = arr.filter(isNotNull);
expect(result).toEqual([1, 2, 3]);
});
it('should preserve type information', () => {
const arr: (string | null)[] = ['a', null, 'b'];
const result = arr.filter(isNotNull);
// TypeScript knows result is string[]
expect(result).toEqual(['a', 'b']);
});
});
describe('getArrayValue', () => {
it('should return value at index', () => {
const arr = ['a', 'b', 'c'];
expect(getArrayValue(arr, 1)).toBe('b');
});
it('should throw on invalid index', () => {
const arr = ['a', 'b'];
expect(() => getArrayValue(arr, 5)).toThrow();
});
it('should use custom error message', () => {
const arr = ['a'];
expect(() => getArrayValue(arr, 2, 'Custom error')).toThrow('Custom error');
});
});
describe('getObjectValue', () => {
it('should return value for key', () => {
const obj = { a: 1, b: 2 };
expect(getObjectValue(obj, 'a')).toBe(1);
});
it('should throw on missing key', () => {
const obj = { a: 1 };
expect(() => getObjectValue(obj, 'b')).toThrow();
});
});
describe('hasProperty', () => {
it('should check property existence', () => {
const obj = { name: 'test' };
expect(hasProperty(obj, 'name')).toBe(true);
expect(hasProperty(obj, 'missing')).toBe(false);
});
});
});All accessor functions throw UnexpectedIndexError when the value is not found:
import { getArrayValue, getObjectValue } from '@lightdash/common';
try {
const value = getArrayValue(myArray, index);
// Process value
} catch (error) {
if (error instanceof UnexpectedIndexError) {
console.error('Value not found at index:', error.message);
}
}