- Abstract class
- Annotations
- Array
- Asserts
- Class
- Const
- Decorators
- Default parameter
- Dictionary
- Enum
- For loop
- forEach()
- Function
- Generics
- Index signature
- Infer
- Inheritance
- Interface
- Let
- Map type
- Mixin
- Module
- Namespace
- Never
- Object type
- Operator
- Optional parameter
- Promise
- Property
- Tuples
- Type alias
- Type guard
- Type narrowing
- Union
- Utility types
- Var
- Void
TYPESCRIPT
TypeScript Type Narrowing: Syntax, Usage, and Examples
TypeScript type narrowing allows you to make your code smarter by reducing a broad type to a more specific one within a certain scope. This improves safety and helps TypeScript give you better autocompletion, error-checking, and code suggestions.
How to Use TypeScript Type Narrowing
You apply type narrowing TypeScript techniques by using conditional logic to check a variable's type, then let TypeScript infer a more specific type based on your condition.
Here’s a basic example:
function printValue(val: string | number) {
if (typeof val === "string") {
console.log(val.toUpperCase()); // val is narrowed to string
} else {
console.log(val.toFixed(2)); // val is narrowed to number
}
}
In this example, the union type string | number
is narrowed using a typeof
check. TypeScript uses this information to treat the variable as a string
or number
inside the respective blocks.
When to Use TypeScript Type Narrowing
You should use type narrowing in TypeScript when:
- You’re working with union types.
- A variable might be null or undefined and needs a check.
- You want to write safer, more predictable conditional logic.
- Your function can accept different data shapes, and you want to handle each one properly.
Narrowing types reduces the risk of runtime errors and makes your code easier to read and maintain.
Examples of Type Narrowing TypeScript Style
Narrowing with typeof
You can narrow primitive types with typeof
.
function handleInput(input: string | number) {
if (typeof input === "string") {
console.log("String length:", input.length);
} else {
console.log("Number squared:", input * input);
}
}
TypeScript knows exactly what type you're working with in each block.
Narrowing with instanceof
Use instanceof
to check if an object was created with a particular class.
class Dog {
bark() {
console.log("Woof!");
}
}
class Cat {
meow() {
console.log("Meow!");
}
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}
This technique helps TypeScript narrow the type to the correct class.
Narrowing with Discriminated Unions
A discriminated union uses a shared literal property to differentiate object types.
type Square = { kind: "square"; size: number };
type Circle = { kind: "circle"; radius: number };
type Shape = Square | Circle;
function area(shape: Shape) {
if (shape.kind === "square") {
return shape.size ** 2;
} else {
return Math.PI * shape.radius ** 2;
}
}
By checking the kind
field, you narrow the type to Square
or Circle
.
Narrowing with in
Operator
The in
operator checks for property existence and narrows accordingly.
type Car = { make: string; model: string };
type Bike = { brand: string; gearCount: number };
function describeVehicle(vehicle: Car | Bike) {
if ("make" in vehicle) {
console.log(`Car: ${vehicle.make} ${vehicle.model}`);
} else {
console.log(`Bike: ${vehicle.brand} with ${vehicle.gearCount} gears`);
}
}
This technique is useful for distinguishing between object types that don’t share a discriminant property.
Narrowing with Null Checks
You can use null or undefined checks to narrow types that include those values.
function greet(user: string | null) {
if (user !== null) {
console.log(`Hello, ${user}`);
} else {
console.log("No user provided");
}
}
This prevents calling string methods on null.
Learn More About Type Narrowing TypeScript Techniques
Custom Type Guards
A custom type guard is a function that helps you narrow a type using a return signature.
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(animal: Fish | Bird) {
if (isFish(animal)) {
animal.swim();
} else {
animal.fly();
}
}
This gives you full control over narrowing logic and makes the code reusable.
Type Predicates
A type predicate has the form parameterName is Type
, and it’s what lets TypeScript apply your custom narrowing logic.
function isString(value: unknown): value is string {
return typeof value === "string";
}
Once TypeScript sees the is
keyword, it knows how to refine the type based on your check.
Exhaustiveness Checks
When you use discriminated unions, it’s smart to handle all possible types. The never
type helps catch unhandled cases.
function getShapeName(shape: Shape): string {
switch (shape.kind) {
case "square":
return "Square";
case "circle":
return "Circle";
default:
const _exhaustive: never = shape;
return _exhaustive;
}
}
If you later add a Triangle
type to your Shape
union, TypeScript will throw an error until you update this function.
Narrowing Inside Loops
TypeScript also applies narrowing within loops:
function logAll(values: (string | number)[]) {
for (const val of values) {
if (typeof val === "string") {
console.log("Uppercased:", val.toUpperCase());
} else {
console.log("Fixed:", val.toFixed(2));
}
}
}
You don't need to cast or check outside the loop—TypeScript narrows inside the block.
Combining Multiple Narrowing Techniques
You can combine typeof
, in
, instanceof
, and custom type guards in complex scenarios.
function process(value: string | number | Date) {
if (typeof value === "string") {
console.log(value.toUpperCase());
} else if (typeof value === "number") {
console.log(value.toFixed(1));
} else if (value instanceof Date) {
console.log(value.toISOString());
}
}
By chaining checks, you give TypeScript all the info it needs to narrow precisely.
Best Practices for TypeScript Type Narrowing
- Keep type checks simple and obvious. Avoid overcomplicated logic that might confuse TypeScript.
- Use discriminated unions wherever possible—they make narrowing much more reliable.
- Write custom type guards for reusable, readable logic.
- Use exhaustiveness checks with
never
to avoid missing any types when handling unions. - Prefer
typeof
andinstanceof
for primitives and class instances. - Avoid overusing type assertions. If you're casting too often, consider whether better narrowing could solve the issue.
Why TypeScript Type Narrowing Improves Your Code
TypeScript type narrowing helps you:
- Catch bugs before they happen.
- Avoid invalid operations (e.g., calling
.toUpperCase()
onnumber
). - Improve code readability and maintainability.
- Let TypeScript assist with better autocomplete and hints.
- Write fewer type assertions or manual casts.
When you write a function that accepts broad types and need to safely operate on more specific ones, narrowing is the way to go.
TypeScript type narrowing turns your broad, flexible types into focused, actionable logic. You guide the compiler using type checks, and TypeScript rewards you with smarter inference and safer code. Whether you’re working with unions, optional values, or third-party data, narrowing gives you the confidence that your code handles each case exactly as intended.
Sign up or download Mimo from the App Store or Google Play to enhance your programming skills and prepare for a career in tech.