{ ILoveJS }

Flatten nested object to dot notation

typescript

A recursive TypeScript function that transforms nested objects into flat objects with dot-notation keys, supporting custom separators and proper type safety.

objectflattentransform

Code

typescript
type Primitive = string | number | boolean | null | undefined;

type FlattenedObject = Record<string, Primitive>;

interface FlattenOptions {
  separator?: string;
  maxDepth?: number;
}

function flattenObject(
  obj: Record<string, unknown>,
  options: FlattenOptions = {}
): FlattenedObject {
  const { separator = ".", maxDepth = Infinity } = options;
  const result: FlattenedObject = {};

  function recurse(
    current: unknown,
    parentKey: string,
    depth: number
  ): void {
    if (
      current === null ||
      current === undefined ||
      typeof current !== "object" ||
      Array.isArray(current) ||
      depth >= maxDepth
    ) {
      result[parentKey] = current as Primitive;
      return;
    }

    const entries = Object.entries(current as Record<string, unknown>);
    
    if (entries.length === 0) {
      result[parentKey] = undefined;
      return;
    }

    for (const [key, value] of entries) {
      const newKey = parentKey ? `${parentKey}${separator}${key}` : key;
      recurse(value, newKey, depth + 1);
    }
  }

  recurse(obj, "", 0);
  return result;
}

function unflattenObject(
  obj: FlattenedObject,
  separator: string = "."
): Record<string, unknown> {
  const result: Record<string, unknown> = {};

  for (const [key, value] of Object.entries(obj)) {
    const keys = key.split(separator);
    let current = result;

    for (let i = 0; i < keys.length - 1; i++) {
      const k = keys[i];
      if (!(k in current) || typeof current[k] !== "object") {
        current[k] = {};
      }
      current = current[k] as Record<string, unknown>;
    }

    current[keys[keys.length - 1]] = value;
  }

  return result;
}

// Example usage
const nestedConfig = {
  database: {
    host: "localhost",
    port: 5432,
    credentials: {
      username: "admin",
      password: "secret"
    }
  },
  features: {
    darkMode: true,
    beta: null
  },
  emptyObject: {},
  arrayValue: [1, 2, 3]
};

console.log("Flattened with dot notation:");
console.log(flattenObject(nestedConfig));

console.log("\nFlattened with custom separator:");
console.log(flattenObject(nestedConfig, { separator: "_" }));

console.log("\nFlattened with max depth of 1:");
console.log(flattenObject(nestedConfig, { maxDepth: 1 }));

const flat = flattenObject(nestedConfig);
console.log("\nUnflattened back:");
console.log(unflattenObject(flat));

How It Works

This implementation uses a recursive approach to traverse nested objects and build flattened keys. The core flattenObject function accepts an object and an optional configuration object that allows customization of the separator character (defaulting to a dot) and maximum recursion depth. The inner recurse function handles the actual traversal, building up keys by concatenating parent keys with child keys using the specified separator.

The type system is carefully designed to handle the complexity of flattening. The Primitive type union represents valid leaf values that cannot be further flattened. The FlattenedObject type uses Record<string, Primitive> to indicate that all values in the result are guaranteed to be non-object primitives. The function accepts Record<string, unknown> as input to allow maximum flexibility while maintaining type safety.

Several edge cases are explicitly handled in the implementation. Null and undefined values are preserved as-is rather than being omitted. Arrays are treated as primitive values and not recursively flattened (since array indices as dot-notation keys would be confusing). Empty objects result in an undefined value being assigned to their key. The maxDepth option prevents infinite recursion on circular references and allows partial flattening when needed.

The companion unflattenObject function reverses the operation, reconstructing nested objects from flattened keys. This is useful for round-trip operations where you need to flatten data for storage or transmission, then reconstruct the original structure. Note that the unflatten operation cannot perfectly restore arrays or distinguish between empty objects and undefined values.

This pattern is particularly useful for working with configuration files, environment variables, form data serialization, and database document storage where flat key structures are preferred. Avoid using this pattern with objects containing circular references (unless using maxDepth), objects where key ordering matters, or when preserving the distinction between empty objects and missing values is important.