Narrowing is the process of moving from a broad type to a more specific type after TypeScript sees a runtime check. A variable may begin as string | number, but inside an if block TypeScript can understand that it is only a string or only a number.
This feature is one of the reasons TypeScript feels natural with JavaScript code. You write normal checks such as typeof, equality checks, property checks, and validation functions. TypeScript follows those checks through the control flow and updates the type automatically.
function formatValue(value: string | number): string {
if (typeof value === "string") {
return value.trim().toUpperCase();
}
return value.toFixed(2);
}
console.log(formatValue(" typescript "));
console.log(formatValue(45.678));
Equality checks are useful when a union contains literal values. For example, many applications model state with values such as "loading", "success", and "error". Checking one value narrows the available shape of the data.
This pattern makes UI states, API states, payment states, and workflow states much clearer than using loose booleans everywhere.
type RequestState =
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; message: string };
function renderState(state: RequestState): string {
if (state.status === "loading") {
return "Loading tutorials...";
}
if (state.status === "error") {
return `Could not load data: ${state.message}`;
}
return `Loaded ${state.data.length} tutorials`;
}
The in operator checks whether a property exists on an object. It is useful when union members do not share the same discriminating property, or when older data shapes are mixed with newer ones.
After TypeScript sees "permissions" in user, it knows the object must be the union member that has a permissions property. This allows the code to access that property safely.
type Admin = { role: "admin"; permissions: string[] };
type Customer = { role: "customer"; orders: number };
function describeUser(user: Admin | Customer): string {
if ("permissions" in user) {
return `Admin with ${user.permissions.length} permissions`;
}
return `Customer with ${user.orders} orders`;
}
console.log(describeUser({ role: "admin", permissions: ["publish", "delete"] }));
JavaScript treats values such as empty strings, 0, null, undefined, and false as falsy. TypeScript understands truthiness checks, but you should use them carefully so you do not accidentally reject valid values like 0.
For optional strings, a truthiness check is often fine. For numbers, prefer explicit checks against null or undefined when zero is a valid value.
function formatDisplayName(name?: string): string {
if (name) {
return name.trim();
}
return "Guest";
}
function formatQuantity(quantity: number | undefined): string {
if (quantity === undefined) {
return "Quantity not selected";
}
return `Quantity: ${quantity}`;
}
console.log(formatQuantity(0)); // Quantity: 0
A custom type guard is a function that returns a special predicate such as value is User. It tells TypeScript that when the function returns true, the checked value should be treated as a specific type.
Custom guards are especially helpful for data that enters your program as unknown: JSON responses, local storage data, query string values, form payloads, or messages from another system.
type User = {
id: number;
name: string;
};
function isUser(value: unknown): value is User {
return typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
typeof (value as { id: unknown }).id === "number" &&
typeof (value as { name: unknown }).name === "string";
}
const data: unknown = { id: 1, name: "Neha" };
if (isUser(data)) {
console.log(data.name.toUpperCase());
}
When a union represents a fixed list of cases, you can use never to make sure every case has been handled. If a new union member is added later and the switch statement is not updated, TypeScript will show an error.
This technique is valuable for code that handles important states, such as payment status, user roles, API responses, and background job results.
type PaymentStatus = "pending" | "paid" | "failed";
function getPaymentLabel(status: PaymentStatus): string {
switch (status) {
case "pending":
return "Waiting for payment";
case "paid":
return "Payment received";
case "failed":
return "Payment failed";
default: {
const neverStatus: never = status;
return neverStatus;
}
}
}
typeof is best for primitives such as strings, numbers, booleans, and functions.
in helps narrow object unions by checking whether a property exists.
unknown data from outside your code.
never can make important union handling exhaustive.
Explore 500+ free tutorials across 20+ languages and frameworks.