TypeScript fails to narrow types inside custom type guard functions when the return type annotation is missing or incorrect. Add the proper type predicate syntax using "is" to enable type narrowing.
This error occurs when you create a function intended to act as a type guard, but TypeScript doesn't recognize it as one. Type guards are special functions that help TypeScript narrow down the type of a variable within a specific scope. Without the proper return type annotation using the type predicate syntax (`parameterName is Type`), TypeScript treats your function as a regular boolean-returning function and doesn't perform any type narrowing. The TypeScript compiler relies on explicit type predicates to understand that a function is checking and validating types. When you write a function that checks if a value is a specific type (like checking `typeof value === 'string'`), you must explicitly tell TypeScript this using the "is" keyword in the return type. Without this annotation, TypeScript has no way to know that your function's true return value guarantees a specific type. This is particularly important when working with union types, filtering arrays, or validating unknown data. The compiler needs your explicit guidance through type predicates to safely narrow types and provide accurate type checking throughout your code.
The most common fix is adding the proper return type annotation using the is keyword. Change your function signature from returning boolean to returning a type predicate:
// ❌ This doesn't narrow types
function isString(value: unknown): boolean {
return typeof value === 'string';
}
// ✅ This narrows the type properly
function isString(value: unknown): value is string {
return typeof value === 'string';
}
const input: unknown = "hello";
if (isString(input)) {
// TypeScript now knows input is string here
console.log(input.toUpperCase());
}The syntax is parameterName is Type where parameterName must exactly match the parameter name in your function signature.
The parameter name in the type predicate must exactly match the parameter name in the function signature:
// ❌ Parameter name mismatch
function isNumber(value: unknown): input is number {
return typeof value === 'number';
}
// ✅ Parameter names match
function isNumber(value: unknown): value is number {
return typeof value === 'number';
}If these don't match, TypeScript won't recognize it as a valid type predicate and won't narrow types.
Your type predicate tells TypeScript to trust that when the function returns true, the value is definitely that type. Make sure your validation logic actually checks this:
// ❌ Logic doesn't match the predicate claim
function isArray(value: unknown): value is Array<any> {
return typeof value === 'object'; // Too broad!
}
// ✅ Logic properly validates the type
function isArray(value: unknown): value is Array<any> {
return Array.isArray(value);
}
// ✅ For more complex types
interface User {
id: number;
name: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
typeof value.id === 'number' &&
'name' in value &&
typeof value.name === 'string'
);
}When filtering arrays, type predicates help narrow the resulting array type:
const mixed: (string | number)[] = [1, "hello", 2, "world"];
// ❌ Without type predicate - result type is still (string | number)[]
const strings = mixed.filter(item => typeof item === 'string');
// ✅ With type predicate - result type is string[]
function isString(value: unknown): value is string {
return typeof value === 'string';
}
const strings = mixed.filter(isString);
// strings is now typed as string[]This is especially useful for filtering out null/undefined values or narrowing union types.
For generic type guards, add proper constraints to ensure type safety:
// ❌ Too permissive
function isType<T>(value: unknown): value is T {
return true; // This is dangerous!
}
// ✅ Use constraints and runtime checks
function hasProperty<T, K extends keyof T>(
obj: T,
key: K
): obj is T & Record<K, NonNullable<T[K]>> {
return obj[key] !== null && obj[key] !== undefined;
}
// ✅ For class instances
function isInstanceOf<T>(
value: unknown,
constructor: new (...args: any[]) => T
): value is T {
return value instanceof constructor;
}If you're using TypeScript 5.5 or later, simple type predicates can be inferred automatically:
// TypeScript 5.5+ infers this is a type predicate automatically
function isString(value: unknown) {
return typeof value === 'string';
}
// But explicit annotation is still clearer and works in all versions
function isString(value: unknown): value is string {
return typeof value === 'string';
}For complex type guards, explicit annotations are still recommended for clarity and compatibility with older TypeScript versions.
Type Predicate Safety: Type predicates are assertions that TypeScript trusts completely. If your logic is wrong (e.g., you claim something is a User but don't properly validate all properties), TypeScript won't catch this at compile time, leading to runtime errors. Always ensure your type guard logic is comprehensive and correct.
Biconditional vs. One-directional: Type predicates are one-directional - they only tell TypeScript what happens when the function returns true. When it returns false, TypeScript doesn't assume the opposite. For example, value is string when true doesn't mean "value is not string" when false if the input type is broader than string | something.
Control Flow Analysis Limitations: Type predicates work well in simple control flow, but can lose effectiveness in complex scenarios like callbacks, async operations, or when the narrowed variable is mutated. TypeScript's control flow analysis tracks narrowing within a scope, but crossing scope boundaries (like in arrow functions) may lose the narrowing.
Performance Consideration: Type predicates have no runtime cost - they're purely compile-time annotations. The runtime cost is only in your validation logic itself.
Generic Type Guards: Be extremely careful with generic type predicates. The pattern function isType<T>(value: unknown): value is T is dangerous because there's no way to validate an arbitrary generic type at runtime without additional type information. Always use constraints or pass runtime type information (like a constructor or schema validator).
Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript