Skip to main content

Advanced Narrowing & Type Concepts

Basic Type Guards

When a function argument can have multiple types, you can use an if statement to check the type and narrow it down.
After narrowing, TypeScript understands the exact type and provides better autocomplete, validation, and error checking.

Example

function printValue(value: string | number) {
if (typeof value === "string") {
// TypeScript knows value is string here
console.log(value.toUpperCase());
} else {
// TypeScript knows value is number here
console.log(value.toFixed(2));
}
}

Common Type Guards

typeof

Used for primitive types.

typeof value === "string"
typeof value === "number"
typeof value === "boolean"

instanceof

Used with classes/objects.

if (date instanceof Date) {
console.log(date.getFullYear());
}

"property" in object

Checks if a property exists.

type User = { name: string };
type Admin = { role: string };

function check(person: User | Admin) {
if ("role" in person) {
console.log(person.role);
}
}

Never Type

The never type represents values that never occur.
It is mainly used for:

  • Functions that always throw an error
  • Functions with infinite loops
  • Exhaustive type checking

Example 1: Function That Throws Error

function throwError(message: string): never {
throw new Error(message);
}

Since the function never successfully finishes, its return type is never.

Example 2: Infinite Loop

function infiniteLoop(): never {
while (true) {
console.log("Running...");
}
}

This function never stops executing.

Example 3: Exhaustive Type Checking

type Shape = "circle" | "square";

function getArea(shape: Shape) {
switch (shape) {
case "circle":
return "Circle Area";

case "square":
return "Square Area";

default:
const neverValue: never = shape;
return neverValue;
}
}

Unknown Type

The unknown type is a safer alternative to any.

You can store any value in unknown, but you must check the type before performing operations on it.

Example

let value: unknown = "Hello TypeScript";

if (typeof value === "string") {
// TypeScript now knows value is a string
console.log(value.toUpperCase());
}

Why Use unknown Instead of any?

With any, TypeScript allows all operations without checking.

let data: any = 10;

data.toUpperCase(); // No error at compile time

This can cause runtime errors.

But with unknown, TypeScript forces you to verify the type first.

let data: unknown = 10;

if (typeof data === "string") {
console.log(data.toUpperCase());
}

This makes your code safer.

Example with Function

function process(value: unknown) {
if (typeof value === "number") {
console.log(value.toFixed(2));
} else {
console.log("Not a number");
}
}

As Casting

Type Assertion (as) in TypeScript

Type assertion in TypeScript allows you to tell the compiler to treat a value as a specific type.
It does not change the actual runtime value — it only helps with type checking.

Syntax

const value = someData as string;

Satisfies

satisfies Operator in TypeScript

The satisfies operator checks whether a value matches a specific type
without changing the inferred type of the value.

It helps ensure type safety while preserving more precise typings.

Syntax

const value = {
name: "Ajay",
age: 25,
} satisfies User;

Example

type Config = {
apiUrl: string;
timeout: number;
};

const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
} satisfies Config;

Discriminated Union

A discriminated union is a pattern where multiple object types share a common property
(called the discriminant) that TypeScript uses to narrow the type safely.

Example-1

type Circle = {
kind: "circle";
radius: number;
};

type Square = {
kind: "square";
size: number;
};

type Shape = Circle | Square;

function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}

return shape.size ** 2;
}

Example-2

function handleShape(shape: Shape) {
switch (shape.kind) {
case "circle":
return shape.radius;

case "square":
return shape.size;

default:
const neverValue: never = shape;
return neverValue;
}
}

Function Overloads

Function overloads allow a function to support multiple parameter types or combinations
while providing accurate type checking and autocomplete.

Syntax

function fn(value: string): string;
function fn(value: number): number;

function fn(value: string | number) {
return value;
}

Example-1

function format(value: string): string;
function format(value: number): string;

function format(value: string | number): string {
if (typeof value === "number") {
return value.toFixed(2);
}

return value.toUpperCase();
}

format(10); // "10.00"
format("hello"); // "HELLO"

Type Predicate Function

Think of a type predicate function as a way to teach TypeScript what your check means.

Normal functions return boolean.
Type predicate functions return value is SomeType.

That tells TypeScript:

If this function returns true, narrow the value to that type.

Why It Exists

Built-in guards are limited to simple checks like:

typeof value === "string"
date instanceof Date
"role" in user

But apps often need custom checks like:

  • Is this API response a User?
  • Is this account an Admin?
  • Is this value safe to treat as a number?

Syntax

function isUser(value: unknown): value is User {
return check;
}

Example 1: Narrow unknown

function isString(value: unknown): value is string {
return typeof value === "string";
}

function printValue(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase());
}
}

Inside if (isString(value)), TypeScript treats value as string.

Example 2: Validate API Data

type User = {
name: string;
email: string;
};

function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
"email" in value
);
}

function handleResponse(data: unknown) {
if (isUser(data)) {
console.log(data.email);
}
}

Useful when data comes from unknown sources.

Example 3: Narrow a Union

type Admin = {
role: "admin";
permissions: string[];
};

type Customer = {
role: "customer";
orders: number[];
};

type Account = Admin | Customer;

function isAdmin(account: Account): account is Admin {
return account.role === "admin";
}

function handleAccount(account: Account) {
if (isAdmin(account)) {
console.log(account.permissions);
} else {
console.log(account.orders);
}
}

Example 4: Filter Arrays

function isNumber(value: unknown): value is number {
return typeof value === "number";
}

const values: unknown[] = [10, "hello", 20, true, 30];

const numbers = values.filter(isNumber);

console.log(numbers); // number[]

Example 5: Optional Field Check

type Product = {
id: number;
discount: number;
};

function hasDiscount(product: Product | { id: number }): product is Product {
return "discount" in product;
}

function printDiscount(product: Product | { id: number }) {
if (hasDiscount(product)) {
console.log(product.discount);
}
}

Usage

Use type predicates when:

  • input starts as unknown
  • unions need custom narrowing
  • the same check repeats in many places
  • filter() should return a narrower array type