TypeScript: Type Narrowing and User-Defined Type Guards
One of the most frequent intermediate to advanced interview questions is:
What is Type Narrowing in TypeScript, and how do you write a custom (User-Defined) Type Guard using the
iskeyword?
TypeScript analyzes your JavaScript control flow to narrow down types automatically inside conditional checks. When built-in helpers are insufficient, you can create custom predicate functions to direct the type checker.
1. Built-in Type Narrowing Techniques
TypeScript understands standard JavaScript runtime operators:
A. The typeof Operator
Used for checking primitive types (string, number, boolean, symbol, undefined, object, function).
function printLength(value: string | number) {
if (typeof value === "string") {
console.log(value.length); // ✅ Type is narrowed to 'string'
} else {
console.log(value.toFixed(2)); // ✅ Type is narrowed to 'number'
}
}B. The instanceof Operator
Used for checking classes, constructor functions, and prototype structures.
function processDate(value: Date | string) {
if (value instanceof Date) {
console.log(value.getTime()); // ✅ Type is narrowed to 'Date'
}
}C. The in Operator
Used to check if an object contains a specific property.
interface Admin {
privileges: string[];
}
interface Employee {
startDate: Date;
}
function printInfo(user: Admin | Employee) {
if ("privileges" in user) {
console.log(user.privileges); // ✅ Type is narrowed to 'Admin'
}
}2. User-Defined Type Guards (parameter is Type)
Sometimes, object shapes cannot be distinguished easily with built-in runtime operators. To solve this, you can write custom validator functions that return a Type Predicate:
Normal boolean return
┌────────────────────────┐
│ function isFish(x) { │ ◄── Returns boolean (Does NOT narrow type)
│ return x.swim !== │
│ undefined; │
│ } │
└────────────────────────┘
Type Predicate return
┌────────────────────────┐
│ function isFish(x): │
│ x is Fish { │ ◄── Returns 'x is Fish' (Forces compiler to narrow)
│ return (x as Fish) │
│ .swim !== │
│ undefined; │
│ } │
└────────────────────────┘Example Code:
interface Cat {
meow(): void;
}
interface Dog {
bark(): void;
}
// User-Defined Type Guard
function isCat(pet: Cat | Dog): pet is Cat {
return (pet as Cat).meow !== undefined;
}
function makeNoise(pet: Cat | Dog) {
if (isCat(pet)) {
pet.meow(); // ✅ TypeScript knows pet is Cat
} else {
pet.bark(); // ✅ TypeScript knows pet must be Dog
}
}3. Discriminated Unions
As a cleaner alternative to custom type guards, you can include a common literal status key (discriminant) in your shapes to narrow types automatically:
interface NetworkSuccessState {
type: "success";
payload: string;
}
interface NetworkErrorState {
type: "error";
code: number;
}
type NetworkState = NetworkSuccessState | NetworkErrorState;
function handle(state: NetworkState) {
switch (state.type) {
case "success":
console.log(state.payload); // Narrowed to NetworkSuccessState
break;
case "error":
console.log(state.code); // Narrowed to NetworkErrorState
break;
}
}Key Takeaways
- Type Predicates: Return
parameterName is Typefrom a function to tell the compiler that returningtrueguarantees the variable type. - Narrowing Operators: Leverage
typeof,instanceof, andinfor standard Javascript object structures. - discriminated unions: Use literal fields to create robust control flows that scale without writing manual checks.
- exhaustive checking: Use the
nevertype in your switch fallbacks to guarantee all union states are handled.