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