🔷 TypeScript Q80 / 101

Explain type predicates.

AI-Powered Answer ✓ Answered

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.

typescript
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.

typescript
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

typescript
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

typescript
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 true incorrectly, you introduce a type safety hole into your application.
  • Parameter Type: The parameter being narrowed must typically be a union type, any, or unknown. 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.