Preserve Types When Transforming Arrays

Array transformations can lose type information if not written carefully. Learn how to preserve types through map, filter, and reduce using type guards, as const, and generics.

6 min read

You preserve types when transforming arrays in TypeScript by preventing the compiler from widening types during inference. A filtered array can keep the original union type. A mapped array might widen literal strings to plain strings, and a reduced array with an empty initial value might end up with no useful type at all.

Each of these problems has a specific fix that preserves precise types through the transformation. The techniques below work at compile time with zero runtime cost.

Preserve literal types with as const

When you create an array of literal values, TypeScript infers the widest reasonable type. The array ["small", "medium", "large"] gets the type string array, not the union of the three literal strings.

Use as const to tell TypeScript to infer the narrowest possible types. This preserves every literal value and marks the array as readonly:

typescripttypescript
let sizes = ["small", "medium", "large"] as const;

TypeScript infers sizes as a readonly tuple of the three specific literal string types. Each element keeps its exact value as its type.

This is useful when the array represents a fixed set of allowed values that you plan to use as a source of truth for union types:

typescripttypescript
let colors = ["red", "green", "blue"] as const;
type Color = (typeof colors)[number];

The Color type resolves to the union of "red", "green", and "blue". The array acts as both the runtime value and the compile-time type source. For more on const assertions, see use as const in TypeScript.

Narrow filter results with type guards

The filter method keeps the original array type with a plain boolean callback. This means filtering out null values still gives you an array typed as (string | null)[] instead of string[].

The fix is to tell TypeScript which type remains after filtering. Do this with a type guard callback that uses the "is" keyword in its return type. A type guard tells the compiler that every surviving element has a specific type.

typescripttypescript
function isPresent<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined;
}
 
let items = ["alpha", null, "beta", undefined, "gamma"];
let clean = items.filter(isPresent);

TypeScript types clean as a string array because the type guard tells the compiler that every surviving element is string, not string or null or undefined. The original array type is preserved and narrowed in one step.

For inline filtering of union arrays, use an arrow function with a type predicate. This works well for separating member types in a union array where each branch gets a precise narrowed result.

typescripttypescript
let mixed: (string | number)[] = ["alpha", 1, "beta", 2];
 
let strings = mixed.filter((v): v is string => typeof v === "string");
let numbers = mixed.filter((v): v is number => typeof v === "number");

Each result has the precise narrowed type. Strings is typed as a string array and numbers as a number array. For more on custom type guards, see custom type guards in TypeScript.

Preserve types through reduce

The reduce method uses the initial value to determine the result type. When the initial value is an empty container, TypeScript has no type information to work with and the result type becomes loose.

Provide an explicit generic type parameter to tell reduce what the result type should be. This sets the accumulator type and the return type in one annotation:

typescripttypescript
let numbers = [1, 2, 3, 4];
 
let result = numbers.reduce<Record<string, number>>((acc, n) => {
  acc[`item${n}`] = n;
  return acc;
}, {});

TypeScript types result as a Record because the generic parameter sets the expected type. Without the generic, the empty object initial value would produce an unhelpful type.

The same pattern works for Map, Set, and custom accumulator types. Always annotate the generic when the initial value is empty or the accumulator type changes during the reduction.

Keep types tight through chained operations

When you chain multiple transformations, each step must preserve its types for the next step to receive precise types. A filter that widens the element type affects every subsequent map and reduce call.

Start the chain with as const if the source array contains literal values. Then use type guard filters to narrow union arrays. End with map or reduce transformations that receive the narrowed types automatically.

The compiler tracks the type through every step. If the first filter narrows (string | number)[] to string[], the next map callback parameter is typed as string without any extra annotation from you.

This is why the order of operations matters: narrowing early in the chain means every later step already works with the tighter type.

Common mistakes

The most frequent mistake is forgetting to use const assertions on a literal array that should drive a union type. Without it, the array type widens to a plain array of strings or numbers, and any derived union type becomes just string or number.

Another common mistake is using a plain boolean callback with filter and expecting the result to be narrowed. The boolean check removes elements at runtime but does not change the compile-time type. Use a type guard callback whenever the filter changes which union members remain.

Avoid using a type assertion to force a narrowed result. The assertion bypasses the type checker and hides real bugs. Use a type guard callback instead -- it is explicit, safe, and checked by the compiler.

Rune AI

Rune AI

Key Insights

  • Use as const on array literals to preserve literal types and readonly tuples.
  • Use type guard callbacks with is to narrow element types through filter.
  • Annotate the generic type parameter on reduce when the initial value is an empty container.
  • Chain transformations confidently when each step preserves its types.
  • Every technique is compile-time only with zero runtime cost.
RunePowered by Rune AI

Frequently Asked Questions

Why do my types get wider after filtering an array?

TypeScript's built-in filter does not narrow the element type with a plain boolean callback. Use a type guard callback with the `is` keyword to preserve the narrowed type through the filter operation.

How do I prevent TypeScript from widening literal types in arrays?

Use `as const` on the array literal. This tells TypeScript to infer the narrowest possible types for each element, including literal strings and readonly tuples, instead of widening to string or number.

Can I chain transformations without losing types in the middle?

Yes, but each step must preserve its types. If filter widens the result, subsequent map calls work with the wider type. Use type guards in filter and annotate reduce generics to keep the chain tight.

Conclusion

Preserving types through array transformations requires understanding where TypeScript widens types and how to prevent it. Use as const for literal arrays, type guards for filter narrowing, and generic parameters on reduce. Each technique keeps more precise type information alive through every step of your transformation pipeline.