Explain type predicates.
TypeScript Type Predicates are a special kind of function return type signature that allows you to narrow down the type of a variable within a certain scope. They are crucial for safely working with union types and for creating custom type guards that inform the TypeScript compiler about the specific type of a variable after a runtime check.
What are Type Predicates?
A type predicate is a function whose return type is of the form parameterName is Type. When such a function is called, and it returns true, TypeScript understands that the parameterName inside the scope where the function was called can now be treated as Type.
Their primary purpose is to enable compile-time type narrowing based on runtime checks. Without type predicates, TypeScript would not be able to infer the type of a variable after a custom function determines its type.
Syntax and Structure
The syntax for a type predicate is parameterName is Type as the return type of a function. The parameterName must be one of the function's parameters, and Type is the specific type you want to narrow to.
function isString(value: any): value is string {
return typeof value === 'string';
}
How Type Predicates Work
When TypeScript sees a function with a type predicate return signature, it understands that if the function evaluates to true, the type of the argument passed to that parameter can be safely narrowed to the specified Type. This allows you to access properties and methods specific to that type without type assertions or errors.
function processValue(value: string | number) {
if (isString(value)) {
// Inside this block, 'value' is narrowed to 'string'
console.log(value.toUpperCase());
} else {
// Inside this block, 'value' is narrowed to 'number'
console.log(value.toFixed(2));
}
}
Common Use Cases
- Narrowing union types (e.g.,
string | number,Bird | Fish). - Creating custom type guards for complex interfaces or classes.
- Filtering arrays of mixed types, such as in
Array.prototype.filter().
Example: Narrowing a Union Type
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
// Type predicate to check if the pet is a Fish
function isFish(pet: Bird | Fish): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function getPetInfo(pet: Bird | Fish) {
if (isFish(pet)) {
// TypeScript now knows 'pet' is 'Fish'
pet.swim();
console.log("It's a fish!");
} else {
// TypeScript now knows 'pet' is 'Bird'
pet.fly();
console.log("It's a bird!");
}
}
const myPet: Bird = { fly: () => console.log('flying'), layEggs: () => console.log('laying eggs') };
getPetInfo(myPet); // Output: flying, It's a bird!
Example: Filtering Arrays
interface Person { name: string; age: number; }
interface Car { make: string; model: string; }
function isPerson(obj: any): obj is Person {
return typeof obj === 'object' && obj !== null && 'name' in obj && 'age' in obj;
}
const items: Array<Person | Car | string> = [
{ name: 'Alice', age: 30 },
'hello',
{ make: 'Toyota', model: 'Camry' },
{ name: 'Bob', age: 25 }
];
// The filter method with a type predicate will narrow the array type
const people = items.filter(isPerson);
// 'people' is now inferred as Person[]
console.log(people);
// Output: [ { name: 'Alice', age: 30 }, { name: 'Bob', age: 25 } ]
Important Considerations
- Runtime Check: Type predicates are only concerned with the *runtime* type. The function body *must* contain actual JavaScript logic to correctly determine the type.
- Truthfulness: It's critical that your type predicate function correctly reflects the type. If a type predicate returns
trueincorrectly, you introduce a type safety hole into your application. - Parameter Type: The parameter being narrowed must typically be a union type,
any, orunknown. If it's already a specific type, the predicate won't provide further narrowing. - Single Parameter: A type predicate can only narrow the type of a single parameter of the function it's defined in.