or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

api

features

charts

charts.mdconditional-formatting.mdvisualizations.md
authorization.mdchangesets.mdcharts-as-code.mdcompiler.mddashboards.mddbt.mdee-features.mdformatting.mdparameters.mdpivot.mdprojects-spaces.mdsql-runner.mdtemplating.mdwarehouse.md
index.md
tile.json

array-utilities.mddocs/api/utilities/specialized/

Array Utilities and Accessor Functions

Utilities for working with arrays, objects, and type-safe data access.

Capabilities

This module provides the following functionality:

Array Utilities

Array Manipulation

/**
 * 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 Guards

/**
 * 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' }]

Accessor Functions

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>;

Examples

Array Access

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"

Object Access

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"

Type-Safe Property Checking

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');
}

Real-World Examples

Processing Query Results

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);
}

Field Access Helper

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');

Safe Array Processing

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}`);
});

Data Validation

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);
}

Chart Data Processing

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}`
      );
    }
  });
}

Testing

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);
    });
  });
});

Error Handling

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);
  }
}

Use Cases

  • Null Filtering: Remove null/undefined values from arrays with type safety
  • Safe Access: Access array and object values with guaranteed existence
  • Data Validation: Validate data structures with type-safe property checks
  • Query Processing: Safely access query results and field values
  • Error Handling: Provide clear error messages for missing data
  • Type Safety: Maintain TypeScript type information through operations

Best Practices

  1. Use isNotNull for filtering instead of manual null checks
  2. Provide custom error messages to accessor functions for better debugging
  3. Catch errors from accessor functions to handle missing data gracefully
  4. Use hasProperty before accessing unknown objects
  5. Prefer accessor functions over direct access when data might be missing

Related Utilities

  • Assertion Utilities: Additional type assertion helpers
  • Item Utilities: Utilities for working with explore items
  • Type Guards: Additional type guard functions