Using Type Predicates for Filtering Arrays in TypeScript
Introduction
Filtering arrays is one of the most common tasks in everyday programming: remove unwanted items, narrow down candidates, and extract useful subsets. In TypeScript, however, naive filtering can silently lose type information. A simple filter returning a boolean doesn't tell TypeScript that the result contains a narrowed type — unless you use type predicates.
This tutorial covers why type predicates matter when filtering arrays, how to write and compose them, and practical patterns you'll reach for in real projects. You will learn:
- How TypeScript's control-flow and type narrowing interact with array methods like Array.prototype.filter.
- The signature and semantics of a type predicate (e.g., x is Foo) and how to declare them for functions and inline callbacks.
- Strategies for filtering arrays of unions, nullable elements, polymorphic collections, and DOM/async iterables.
- How to combine predicates, reuse predicates as higher-order functions, and check runtime safety with type guards.
By the end of this guide you'll be able to write robust filters that preserve or refine types without casting, leading to safer code and fewer runtime surprises. We'll include many hands-on examples, common pitfalls, and performance/maintainability tips for intermediate TypeScript developers.
Background & Context
Type predicates in TypeScript are syntactic markers on function return types that tell the compiler "this value has a more specific type if the function returns true." The canonical form is function isFoo(x: unknown): x is Foo and is often called a type guard. When used with conditional checks and array methods, they enable the compiler to narrow element types in the resulting arrays so you can safely access properties that belong only to the narrowed type.
Understanding type predicates unlocks safer filtering of unions (e.g., (A | B)[]), arrays that may contain null or undefined, and scenarios where functions are used as callbacks for filter, map and other higher-order methods. These patterns are also relevant when dealing with iterables or async iterables where you want to yield only elements of particular shapes.
If you need deeper knowledge about how to type functions that accept variable arguments (useful for predicate factories) or how to type iterators and async iterables that you may also want to filter, these related guides are helpful: Typing Functions That Accept a Variable Number of Arguments (Tuples and Rest) and Typing Iterators and Iterables in TypeScript.
Key Takeaways
- Type predicates (x is T) inform the TypeScript compiler about runtime checks and preserve narrowed types after filtering.
- Use explicit predicate functions for complex logic and inline predicates for simple checks.
- Prefer reusable predicate factories to avoid duplication and to compose filters safely.
- When filtering iterables or async iterables, use typed iterator helpers and preserve types with predicate signatures.
- Watch out for TypeScript inference edge cases; sometimes explicit generics or overloads are needed.
Prerequisites & Setup
This guide expects you to be comfortable with TypeScript basics (types, unions, generics, and function signatures) and a modern TypeScript toolchain (TS 4.x+). Ensure your tsconfig enables strict type checking (recommended) so you get the most value from type predicates.
Install TypeScript (if needed) and initialize a project:
npm init -y npm install --save-dev typescript npx tsc --init
Set strict: true in tsconfig.json. Examples in this article will compile with TypeScript 4.5+; if you work with older versions, some inference improvements might not be available.
Main Tutorial Sections
1. What is a Type Predicate? (Quick Primer)
A type predicate is a return type of the form paramName is Type. Example:
function isString(x: unknown): x is string {
return typeof x === 'string';
}
const arr: unknown[] = [1, 'a', "hello", null];
const strings = arr.filter(isString); // type: string[]After filter(isString) TypeScript knows strings is string[] because isString tells the compiler each element that returns true is a string. Use predicates anytime you want runtime checks to produce static type narrowing.
Related reading on function parameter typing and rest/tuple patterns can be useful when building predicate factories: Typing Function Parameters as Tuples in TypeScript and Typing Functions That Accept a Variable Number of Arguments (Tuples and Rest).
2. Filtering Nullables and Optional Values
A common task is removing null and undefined values from arrays. A compact predicate:
function notNull<T>(v: T | null | undefined): v is T {
return v !== null && v !== undefined;
}
const maybeNums: Array<number | null | undefined> = [1, null, 3, undefined];
const nums: number[] = maybeNums.filter(notNull);This pattern avoids non-null assertions (!) and preserves precise types for later processing. When working with JSON, BigInt conversions, or built-ins, consider the conversion and safety implications described in Typing BigInt in TypeScript: Practical Guide for Intermediate Developers.
3. Filtering Unions with Discriminants
For discriminated unions, predicates typically check the discriminant property:
type Animal = { kind: 'dog'; bark(): void } | { kind: 'cat'; meow(): void };
function isDog(a: Animal): a is Extract<Animal, { kind: 'dog' }> {
return a.kind === 'dog';
}
const animals: Animal[] = [{ kind: 'dog', bark(){} }, { kind: 'cat', meow() {} }];
const dogs = animals.filter(isDog); // dogs: { kind: 'dog'; bark(): void }[]This keeps code safe and readable. For patterns that involve accessor methods or getters you might also want to review Typing Getters and Setters in Classes and Objects for correct class typing.
4. Inline Predicates vs Named Predicates
You can pass inline arrow functions to filter, but TypeScript won't infer a predicate type unless the function's return type is declared as a predicate. Example:
const items: (string | number | null)[] = ['a', 1, null];
// Inline boolean check — loses narrowing
const justStrings1 = items.filter(x => typeof x === 'string'); // (string | number | null)[]
// Named predicate — preserves narrowing
function isString(x: unknown): x is string { return typeof x === 'string'; }
const justStrings2 = items.filter(isString); // string[]If you prefer inline style, annotate the callback explicitly:
const justStrings3 = items.filter((x): x is string => typeof x === 'string'); // string[]
Explicit predicate syntax is preferable for clarity and type-safety.
5. Composing Predicates (Higher-Order Guards)
Compose predicates to build complex filters while keeping them reusable:
function and<A>(pa: (x: A) => boolean, pb: (x: A) => boolean) {
return (x: A) => pa(x) && pb(x);
}
// With predicates that narrow, declare the signature precisely
function andGuard<T, U extends T>(pa: (x: T) => x is U, pb: (x: U) => boolean) {
return (x: T): x is U => pa(x) && pb(x as U);
}
// Example usage
function isNonEmptyString(x: unknown): x is string { return typeof x === 'string' && x.length > 0; }
const arr: unknown[] = ['a', '', 0];
const nonEmptyStrings = arr.filter(isNonEmptyString); // string[]When constructing powerful combinators you may run into function-typing complexities—reading about advanced function typing like rest/tuple parameter patterns can help: Typing Functions That Accept a Variable Number of Arguments (Tuples and Rest).
6. Filtering Iterables and Custom Iterators
Predicates aren't limited to arrays. When working with iterables you may produce filtered iterators that preserve types:
function* filterIter<T>(it: Iterable<T>, pred: (t: T) => boolean): Iterable<T> {
for (const x of it) if (pred(x)) yield x;
}
// With predicate type
function* filterIterGuard<T, U extends T>(it: Iterable<T>, pred: (t: T) => t is U): Iterable<U> {
for (const x of it) if (pred(x)) yield x as U;
}Typing iterables is subtly different than arrays; to build robust helpers refer to Typing Iterators and Iterables in TypeScript for patterns and edge cases. Similarly, if you need to filter async streams, see Typing Async Iterators and Async Iterables in TypeScript — Practical Guide.
7. Filtering Promises & Async Predicates
When predicates perform async checks (e.g., database queries), you can't use Array.prototype.filter directly. Instead, combine map + filter(Boolean) or use asynchronous helpers:
async function asyncFilter<T>(arr: T[], pred: (t: T) => Promise<boolean>) {
const results = await Promise.all(arr.map(async t => [t, await pred(t)] as const));
return results.filter(([, ok]) => ok).map(([t]) => t);
}
// Example
async function existsInDB(id: number): Promise<boolean> { /* ... */ return true; }
const ids = [1,2,3];
const existing = await asyncFilter(ids, existsInDB);If your async workflow yields typed async iterables, combine predicate checks with typed async iterators for streaming filters. See Typing Async Iterators and Async Iterables in TypeScript — Practical Guide for guidance.
8. Predicates with this and Method Guards
Predicates bound to object instances or classes may need this typing. Use ThisParameterType / OmitThisParameter patterns when you create functions that rely on this for checks. Example:
class Container {
constructor(private items: unknown[]) {}
filterStrings(this: Container) {
return this.items.filter((x): x is string => typeof x === 'string');
}
}If you extract methods into standalone functions, pay attention to this binding. For strategies on typing functions that modify or use this, see Typing Functions That Modify this (ThisParameterType, OmitThisParameter).
9. Interoperability with 3rd-Party Libraries
When filtering values from third-party APIs, you may not have perfect types. Combining runtime guards with declaration merging or wrapper functions is a pragmatic approach:
// Third-party returns unknown[]
declare function fetchItems(): Promise<unknown[]>;
async function getValid(items: unknown[]) {
return items.filter((x): x is { id: number } => !!x && typeof (x as any).id === 'number');
}When integrating with complex external libs, consult strategies in Typing Third-Party Libraries with Complex APIs — A Practical Guide about runtime guards and shaping types.
Advanced Techniques
Once you get comfortable with basic predicates, consider these expert techniques:
-
Predicate Factories with Generics: build parameterized guards (e.g.,
isOfKind<T extends string>(kind: T)returns a predicate narrowing to the union member with that kind). Use precise generics to preserve inference. -
Distributive Conditional Types: combine predicates with conditional types to derive new types from unions programmatically.
-
Predicate Composition Libraries: centralize common guards into a small library and export typed combinators—this reduces duplication and increases testability.
-
Performance Considerations: avoid expensive checks inside tight loops. If a predicate is heavy, consider precomputing indexes or using memoization. For lazy consumption, filter iterables instead of arrays to reduce allocations; see Using for...of and for await...of with Typed Iterables in TypeScript for efficient iteration patterns.
-
Type-level Testing: add compile-time unit tests (using tsd or type challenges) to ensure your predicate types behave as expected after code changes.
Best Practices & Common Pitfalls
Do:
- Prefer named predicates when the check is repeated or non-trivial; it improves reuse and readability.
- Annotate inline predicates when you want narrowing:
(x): x is Foo => .... - Use
Extract<T, U>to express narrowed union members cleanly.
Don't:
- Use
ascasts to silence the compiler instead of writing a real guard — you lose safety. - Mix runtime exceptions and type predicates in ways that confuse control flow. Predicates must be pure boolean checks for predictable narrowing.
Pitfalls:
- Arrow functions without an explicit predicate return type do not narrow the resulting array's type. Remember to annotate them when needed.
- Predicates that mutate input or throw will break the assumption "returns true implies value has type T". Keep guards side-effect free.
If you're dealing with DOM elements as the items you want to filter, make sure you correctly type DOM references to avoid runtime issues—see Typing DOM Elements and Events in TypeScript (Advanced) for DOM-specific patterns.
Real-World Applications
- Sanitizing API responses: filter unknown arrays into typed objects with predicates before mapping to models.
- Event filtering: narrow event types in an event bus so subscribers only receive validated payloads.
- Form validation: filter input arrays of optional fields to the non-null set for processing.
- Stream processing: apply predicates to iterables or async iterables to drop invalid items before downstream consumption — combine with typed iterator helpers as in Typing Async Iterators and Async Iterables in TypeScript — Practical Guide.
These patterns also tie into class design: when you store typed items in containers or static members, refer to Typing Static Class Members in TypeScript: A Practical Guide to ensure the class-level API remains type-safe when filtered data flows through.
Conclusion & Next Steps
Type predicates are a small but powerful feature of TypeScript that dramatically improves safety when filtering collections. Start by replacing casts and ! assertions with well-defined predicates, then evolve your codebase by composing, testing, and optimizing those predicates. Next steps: practice building predicate libraries, apply them to iterables/streams, and add type-level tests.
Suggested follow-ups: review function parameter typing and iterator typing to make predicate factories and iterators robust — check out Typing Function Parameters as Tuples in TypeScript and Typing Iterators and Iterables in TypeScript.
Enhanced FAQ
Q: What is the basic syntax of a type predicate?
A: A type predicate is written as paramName is Type and used as the return type of a function. Example:
function isNumber(x: unknown): x is number {
return typeof x === 'number';
}When this function returns true, TypeScript narrows the type of the checked variable to number in the compiler's control flow analysis.
Q: Can I use arrow functions as predicates?
A: Yes, but you must annotate the arrow function's return type as a predicate for TypeScript to narrow types.
const isNum = (x: unknown): x is number => typeof x === 'number';
If you omit the : x is number part, TypeScript treats it as a plain boolean-returning function and won't narrow.
Q: Why does arr.filter(x => typeof x === 'string') not give me string[]?
A: The inline arrow callback is typed as (x: T) => boolean, not a predicate. The compiler needs the predicate return type x is string to narrow the element type of the filtered array. Either provide a named predicate or annotate the inline function.
Q: Can a predicate mutate the input or depend on external state?
A: Predicates should ideally be pure boolean checks. If a predicate mutates input or throws, it undermines assumptions the compiler and developers make. If external state is required, keep side-effects separate and well-documented, and prefer returning a safe boolean.
Q: How do predicates work with generics and union types?
A: Predicates can be generic: function isOfType<T>(x: unknown, check: (u: unknown) => u is T): u is T patterns are possible, but require careful typing. When narrowing union types, predicates typically check discriminants and use Extract<T, {discriminant: Value}> to express the narrowed type.
Q: What about filtering iterables or async iterables?
A: Use typed iterator helpers that accept predicate signatures and yield narrowed types. For async scenarios, gather predicate results with Promise.all or create async filters that iterate and await the predicate. Check Typing Async Iterators and Async Iterables in TypeScript — Practical Guide for deeper recipes.
Q: How should I test predicates?
A: Test both runtime behavior and type behavior. Runtime tests ensure correctness; type tests (using tsd or test files that assert expectType) ensure you don't accidentally regress the narrowing behavior.
Q: Can predicates be used with third-party libraries that return any or unknown?
A: Absolutely. Predicates are particularly useful as a defensive layer around third-party data. When wrapping library functions or parsing JSON, validate and narrow values with predicates before handing them to typed internal APIs. For patterns on typing third-party libs, see Typing Third-Party Libraries with Complex APIs — A Practical Guide.
Q: Are there performance concerns using many predicates in a loop?
A: A predicate's runtime cost depends on the checks it performs. Simple type checks are cheap. For heavy predicates (e.g., regex, database lookups), avoid running them inside hot loops; consider precomputing indices, caching results, or streaming filters with lazy iterables to reduce work. For iteration patterns and performance, read Using for...of and for await...of with Typed Iterables in TypeScript.
Q: How do predicates interact with class this and methods?
A: When predicates rely on this, ensure this is typed correctly, either by using this: Type in the method signature or by using utility types like ThisParameterType and OmitThisParameter when extracting methods. Reference Typing Functions That Modify this (ThisParameterType, OmitThisParameter) for recommended patterns.
Q: Where should I place my predicate functions in a codebase?
A: Organize commonly used predicates in a utility module or a domain-specific module (e.g., guards.ts or validators/*.ts). Keep domain predicates close to the types they validate (e.g., userGuards.ts next to user.ts). This improves discoverability and encourages reuse.
Q: Any tips for debugging type narrowing issues?
A: Use small reproductions — isolate the predicate and its use in a sandbox. Add explicit return type annotations to see compiler errors. Tools like TypeScript's --noImplicitAny and --strict expose inference gaps. If you see unexpected types, hover in your editor and consider adding as const or generic annotations to aid inference.
If you want more examples about advanced function typing when composing predicate factories, check Typing Function Parameters as Tuples in TypeScript. For filtering objects keyed by symbols or working with advanced keying strategies, Typing Symbols as Object Keys in TypeScript — Comprehensive Guide can help inform your design choices. Finally, if you're filtering data that ends up in class constructors or static members, review Typing Class Constructors in TypeScript — A Comprehensive Guide and Typing Static Class Members in TypeScript: A Practical Guide for ensuring types remain consistent across boundaries.
By integrating type predicates into your toolkit you make array and iterable filtering both safer and clearer — a high-leverage improvement for intermediate TypeScript developers.
