Type narrowing is the process of reducing a value from a broader type, such as a union type, to a more specific type. TypeScript uses checks in your code to understand which type a value has at a particular point.
Type narrowing means changing a broad type into a more specific type based on a condition.
function printValue(
value: string | number
) {
if (typeof value === "string") {
console.log(
value.toUpperCase()
);
}
}
Inside the if block, TypeScript knows that
value is a string.
A union type can contain different types. Some operations may be available only on one of those types.
function showValue(
value: string | number
) {
// value.toUpperCase();
// Error because value
// may be a number.
}
We narrow the type before using type-specific operations.
The typeof operator is one of the most common
ways to narrow primitive types.
function showValue(
value: string | number
) {
if (typeof value === "string") {
console.log(
value.toUpperCase()
);
} else {
console.log(
value.toFixed(2)
);
}
}
When TypeScript knows that a value is a string, string methods become available.
function display(
value: string | number
) {
if (typeof value === "string") {
console.log(
value.toUpperCase()
);
console.log(
value.length
);
}
}
TypeScript can also narrow a union to a number.
function display(
value: string | number
) {
if (typeof value === "number") {
console.log(
value.toFixed(2)
);
}
}
Boolean values can also be narrowed using
typeof.
function show(
value: string | boolean
) {
if (typeof value === "boolean") {
console.log(
value
);
}
}
Common results of the JavaScript typeof
operator include:
"string""number""boolean""undefined""object""function"const value = "Hello";
if (typeof value === "string") {
console.log("It is a string");
}
An if...else statement can divide a union
into different possible types.
function showValue(
value: string | number
) {
if (typeof value === "string") {
console.log(
"String:",
value
);
} else {
console.log(
"Number:",
value
);
}
}
Multiple type checks can be used for a union containing several types.
function showValue(
value:
string |
number |
boolean
) {
if (typeof value === "string") {
console.log(
"String"
);
} else if (
typeof value === "number"
) {
console.log(
"Number"
);
} else {
console.log(
"Boolean"
);
}
}
Array.isArray() can help narrow a value
to an array.
function showData(
value: string | string[]
) {
if (Array.isArray(value)) {
console.log(
value.length
);
} else {
console.log(
value.toUpperCase()
);
}
}
The in operator can help distinguish between
object types based on their properties.
interface Student {
name: string;
course: string;
}
interface Teacher {
name: string;
subject: string;
}
function showUser(
user: Student | Teacher
) {
if ("course" in user) {
console.log(
user.course
);
} else {
console.log(
user.subject
);
}
}
The in operator checks whether a property
exists in an object.
interface Admin {
name: string;
permissions: string[];
}
interface Student {
name: string;
course: string;
}
function showUser(
user: Admin | Student
) {
if ("permissions" in user) {
console.log(
user.permissions
);
}
}
The instanceof operator can narrow values
involving classes.
class Student {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Teacher {
subject: string;
constructor(subject: string) {
this.subject = subject;
}
}
function show(
person: Student | Teacher
) {
if (person instanceof Student) {
console.log(
person.name
);
} else {
console.log(
person.subject
);
}
}
Equality checks can also narrow a union type.
function showValue(
value: string | number
) {
if (value === "Amit") {
console.log(
"The value is Amit"
);
} else {
console.log(
value
);
}
}
Literal properties can be used to narrow object unions.
interface Admin {
role: "admin";
name: string;
}
interface Student {
role: "student";
name: string;
}
type User =
Admin | Student;
function showUser(
user: User
) {
if (user.role === "admin") {
console.log(
"Admin:",
user.name
);
} else {
console.log(
"Student:",
user.name
);
}
}
A discriminated union uses a common literal property to distinguish between different object types.
interface SuccessResponse {
status: "success";
data: string;
}
interface ErrorResponse {
status: "error";
message: string;
}
type Response =
SuccessResponse |
ErrorResponse;
function handleResponse(
response: Response
) {
if (
response.status ===
"success"
) {
console.log(
response.data
);
} else {
console.log(
response.message
);
}
}
A null check can narrow a value that may be
null.
function showName(
name: string | null
) {
if (name !== null) {
console.log(
name.toUpperCase()
);
} else {
console.log(
"No name"
);
}
}
You can check for undefined before using
an optional value.
function showPhone(
phone: string | undefined
) {
if (phone !== undefined) {
console.log(
phone
);
} else {
console.log(
"Phone not available"
);
}
}
A truthiness check can narrow values that may be
null, undefined, or another
falsy value.
function showName(
name: string | null
) {
if (name) {
console.log(
name.toUpperCase()
);
} else {
console.log(
"No name"
);
}
}
Optional chaining can safely access a property when an object may be undefined.
interface User {
name: string;
address?: {
city: string;
};
}
function showCity(
user: User
) {
console.log(
user.address?.city
);
}
A custom type guard is a function that tells TypeScript whether a value belongs to a specific type.
function isString(
value: unknown
): value is string {
return typeof value === "string";
}
const value: unknown =
"Hello";
if (isString(value)) {
console.log(
value.toUpperCase()
);
}
The unknown type requires narrowing before
many operations can be performed.
function showValue(
value: unknown
) {
if (typeof value === "string") {
console.log(
value.toUpperCase()
);
} else if (
typeof value === "number"
) {
console.log(
value.toFixed(2)
);
}
}
TypeScript can narrow object properties based on conditions.
interface User {
name: string;
age?: number;
}
function showUser(
user: User
) {
if (user.age !== undefined) {
console.log(
user.age.toFixed(0)
);
}
}
Function parameters with union types can be narrowed inside the function.
function calculate(
value: number | string
) {
if (typeof value === "number") {
return value * 2;
}
return value.length;
}
console.log(
calculate(10)
);
console.log(
calculate("Hello")
);
Array values can also be narrowed when their type is a union.
function process(
value: string | number[]
) {
if (Array.isArray(value)) {
value.forEach(
number => {
console.log(number);
}
);
} else {
console.log(
value.toUpperCase()
);
}
}
A common mistake is using a method without first checking whether the value supports that method.
function show(
value: string | number
) {
// Incorrect:
// console.log(
// value.toUpperCase()
// );
if (typeof value === "string") {
console.log(
value.toUpperCase()
);
}
}
Always narrow a union before using a type-specific operation.
Type narrowing is useful whenever a value has multiple possible types or structures.
TypeScript provides several ways to narrow types.
typeofinstanceofinArray.isArray()interface Student {
type: "student";
name: string;
course: string;
}
interface Teacher {
type: "teacher";
name: string;
subject: string;
}
type User =
Student | Teacher;
function showUser(
user: User
) {
console.log(
"Name:",
user.name
);
if (user.type === "student") {
console.log(
"Course:",
user.course
);
} else {
console.log(
"Subject:",
user.subject
);
}
}
showUser({
type: "student",
name: "Amit",
course: "TypeScript"
});
showUser({
type: "teacher",
name: "Rahul",
subject: "JavaScript"
});
Type narrowing allows TypeScript to determine a more specific type from a broader type.
function showValue(
value: string | number
) {
if (typeof value === "string") {
console.log(
value.toUpperCase()
);
} else {
console.log(
value.toFixed(2)
);
}
}
By narrowing types before using them, your code becomes safer and TypeScript can provide better type checking and autocomplete.
typeof can narrow primitive types.instanceof can narrow class instances.in operator can narrow object types.Array.isArray() can narrow arrays.unknown.Question: Which operator is commonly used to narrow primitive types such as string and number?