Type Guards and Advanced Types
Level: Beginner
- Type guards with typeof and instanceof
- Discriminated unions for safe pattern matching
- User-defined type predicates (is keyword)
- Type narrowing in if/else branches
- Switch statements with exhaustive checks
- Asserts for narrowing assumptions
- Conditional types for advanced patterns
- SMS status and role type checking
Why This Matters
Type Guards and Advanced Types helps you write safer frontend code. In ASP.NET Core projects, TypeScript makes API response shapes, form models, DOM values, and reusable helpers easier to maintain.
Narrow types safely and handle complex scenarios.
The Problem
JavaScript allows many mistakes to appear only at runtime. This lesson shows how Type Guards and Advanced Types lets TypeScript catch wrong values, missing properties, and unsafe assumptions before the page reaches users.
Type Checking
function processValue(value: string | number): void {
if (typeof value === 'string') {
// Inside: value is string
console.log(value.toUpperCase());
} else {
// Inside: value is number
console.log(value.toFixed(2));
}
}
Instance Checking
class Student {
name: string;
constructor(name: string) {
this.name = name;
}
}
function printName(obj: any): void {
if (obj instanceof Student) {
// Now: obj is Student
console.log(obj.name);
} else {
// obj is something else
console.log('Not a student');
}
}
Discriminated Unions
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
function handleResult<T>(result: Result<T>): void {
if (result.success) {
// Inside: success is true, data exists
console.log(result.data);
} else {
// Inside: success is false, error exists
console.error(result.error);
}
}
const studentResult: Result<string> = {
success: true,
data: "Ravi Kumar"
};
handleResult(studentResult);
Type Predicates
function isStudent(obj: any): obj is Student {
return obj && typeof obj.name === 'string' && typeof obj.id === 'number';
}
function process(obj: any): void {
if (isStudent(obj)) {
// Now TypeScript knows obj is Student
console.log(obj.name, obj.id);
}
}
Optional Chaining and Nullish Coalescing
interface Student {
id: number;
name: string;
address?: { city: string };
}
const student: Student = { id: 101, name: "Ravi" };
// Optional chaining - safe null access
const city = student.address?.city; // undefined if address doesn't exist
// Nullish coalescing - default if null/undefined
const displayCity = student.address?.city ?? 'Unknown City';
Generic Constraints with Type Guards
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] | string {
const value = obj[key];
if (value === null || value === undefined) {
return 'Not set';
}
return value;
}
interface Student {
name: string;
email?: string;
}
const student: Student = { name: "Ravi" };
const value = getProperty(student, 'email'); // string | undefined
Real API Response Types
// Different response types based on status
type ApiResponse<T> =
| { status: 'success'; data: T }
| { status: 'error'; code: number; message: string }
| { status: 'loading' };
async function fetchStudent(id: number): Promise<ApiResponse<Student>> {
try {
const response = await fetch(`/api/students/${id}`);
if (!response.ok) {
return {
status: 'error',
code: response.status,
message: response.statusText
};
}
const data = await response.json();
return {
status: 'success',
data
};
} catch (error) {
return {
status: 'error',
code: 0,
message: error instanceof Error ? error.message : 'Unknown error'
};
}
}
// Usage
const result = await fetchStudent(101);
if (result.status === 'success') {
console.log(result.data.name); // TypeScript knows data exists
} else if (result.status === 'error') {
console.error(result.message); // TypeScript knows message exists
}
SMS Example
interface StudentSuccess {
type: 'student';
id: number;
name: string;
className: string;
}
interface ExamSuccess {
type: 'exam';
examId: number;
studentId: number;
marks: number;
}
type ApiData = StudentSuccess | ExamSuccess;
function displayData(data: ApiData): void {
switch (data.type) {
case 'student':
console.log(`Student: ${data.name} in class ${data.className}`);
break;
case 'exam':
console.log(`Exam: Student ${data.studentId} got ${data.marks}`);
break;
}
}
// Safely handle different types
const studentData: StudentSuccess = {
type: 'student',
id: 101,
name: 'Ravi Kumar',
className: '10'
};
displayData(studentData);
Key Takeaways
- Type guards narrow types safely
- Discriminated unions for complex data
- Optional chaining for null safety
- Type predicates for custom checks
- Real API responses use these patterns
- Next: Complete SMS example
Discriminated unions are powerful for API responses with different success/error formats. Always discriminate on a literal type.
- Not using type guards — Access wrong properties
- Wrong discriminator — Union doesn't narrow
- Forgetting null checks — Optional chaining not used
- Overly broad unions — Hard to narrow down
Use ChatGPT, Claude, or Copilot to go deeper on Type Guards. Try these prompts:
"When use discriminated unions?""Why use optional chaining?""How do type predicates work?""Quiz me on type guards"
💡 Tip: After reading this article, paste your own code into AI and ask "What could go wrong here and why?" — fastest way to find edge cases and deepen understanding.
Quick Definitions
- Type Guards and Advanced Types - The main TypeScript concept explained in this lesson.
- Type - A rule that describes what kind of value is allowed.
- Interface - A contract that describes the shape of an object.
- DTO model - The frontend shape of data coming from ASP.NET Core Web API.
Common Mistakes
- Using
anyinstead of defining a useful type - Making types too complex before understanding the data
- Forgetting that API data still needs runtime validation
- Confusing JavaScript runtime behavior with TypeScript compile-time checks
- Not reusing shared types for repeated API models
Practice Task
Create a small TypeScript example using Type Guards and Advanced Types. Keep it connected to a School Management System scenario.
Suggested practice:
- Define a
Student,Teacher,Attendance, orMarksmodel. - Write one function or class using that type.
- Add one intentionally wrong value and read the TypeScript error.
- Fix the type or the data shape.
- Explain the type contract in your own words.
Quick Revision
| Question | Answer |
|---|---|
| What is the main idea? | Use TypeScript to make Type Guards and Advanced Types safer and clearer. |
| Where is it used? | API models, forms, DOM code, helpers, and React/Next.js apps. |
| What should beginners avoid? | Overusing any and ignoring API validation. |
| What is the best debugging habit? | Read the TypeScript error and compare it with the expected data shape. |
Type Guards and Advanced Types is a TypeScript concept that improves JavaScript safety by making data shapes and function expectations clear. I would explain the problem it solves, show a small example, and mention how it helps with API-connected frontend code.
It is used in API DTOs, form models, DOM access, helper functions, React props, service classes, and reusable frontend utilities.