Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
menu
  1. Home
  2. /Web Development
  3. /Typescript
  4. /Type Guards Narrowing
Your Progress0%
0 of 30 completed

TypeScript Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices

Type Guards and Type Assertions

Safe type narrowing and runtime type checking

Type guards help you narrow types safely at runtime, while type assertions let you override TypeScript's type inference. Type guards are safe and recommended; type assertions bypass safety checks and should be used carefully. Understanding both is crucial for working with dynamic data, APIs, and complex type scenarios. Let's master type guards and learn when assertions are appropriate!

Built-in Type Guards

TypeScript provides several built-in type guards that automatically narrow types based on runtime checks.

typeof Type Guard

typeof-guard.ts
// typeof narrows primitive types
function processValue(value: string | number | boolean): string {
  if (typeof value === "string") {
    // TypeScript knows value is string here
    return value.toUpperCase();
  }
  
  if (typeof value === "number") {
    // TypeScript knows value is number here
    return value.toFixed(2);
  }
  
  // TypeScript knows value is boolean here
  return value ? "Yes" : "No";
}

console.log(processValue("hello"));   // "HELLO"
console.log(processValue(123.456));   // "123.46"
console.log(processValue(true));      // "Yes"

// typeof with unknown
function parseInput(input: unknown): string {
  if (typeof input === "string") {
    return input;
  }
  
  if (typeof input === "number") {
    return input.toString();
  }
  
  if (typeof input === "boolean") {
    return input ? "true" : "false";
  }
  
  return "unknown";
}

console.log(parseInput("text"));      // "text"
console.log(parseInput(42));          // "42"
console.log(parseInput(true));        // "true"
console.log(parseInput({}));          // "unknown"

// Multiple typeof checks
function formatValue(value: string | number | null | undefined): string {
  if (typeof value === "undefined") {
    return "Not provided";
  }
  
  if (value === null) {
    return "Null value";
  }
  
  if (typeof value === "string") {
    return `"${value}"`;
  }
  
  return value.toFixed(2);
}

console.log(formatValue(undefined));  // "Not provided"
console.log(formatValue(null));       // "Null value"
console.log(formatValue("hello"));    // ""hello""
console.log(formatValue(99.99));      // "99.99"

instanceof Type Guard

instanceof-guard.ts
// instanceof narrows class instances
class User {
  constructor(public name: string, public email: string) {}
  
  displayInfo(): void {
    console.log(`User: ${this.name} (${this.email})`);
  }
}

class Admin extends User {
  constructor(
    name: string,
    email: string,
    public permissions: string[]
  ) {
    super(name, email);
  }
  
  displayInfo(): void {
    console.log(`Admin: ${this.name} (${this.email})`);
    console.log(`Permissions: ${this.permissions.join(", ")}`);
  }
}

function processAccount(account: User | Admin): void {
  if (account instanceof Admin) {
    // TypeScript knows account is Admin here
    console.log(`Admin with ${account.permissions.length} permissions`);
    account.displayInfo();
  } else {
    // TypeScript knows account is User here
    account.displayInfo();
  }
}

const user = new User("Yusuf", "yusuf@example.com");
const admin = new Admin("Amina", "amina@example.com", ["read", "write", "delete"]);

processAccount(user);
processAccount(admin);

// instanceof with built-in types
function handleValue(value: Date | RegExp | Error): string {
  if (value instanceof Date) {
    return value.toISOString();
  }
  
  if (value instanceof RegExp) {
    return value.source;
  }
  
  // TypeScript knows value is Error here
  return value.message;
}

console.log(handleValue(new Date()));           // "2024-12-25T10:30:00.000Z"
console.log(handleValue(/[a-z]+/));            // "[a-z]+"
console.log(handleValue(new Error("Oops")));   // "Oops"

// instanceof with arrays
function processData(data: string[] | Set<string> | Map<string, string>): number {
  if (data instanceof Array) {
    return data.length;
  }
  
  if (data instanceof Set) {
    return data.size;
  }
  
  // TypeScript knows data is Map here
  return data.size;
}

console.log(processData(["a", "b", "c"]));                    // 3
console.log(processData(new Set(["x", "y", "z"])));          // 3
console.log(processData(new Map([["k1", "v1"], ["k2", "v2"]])));  // 2

in Operator Type Guard

in-operator-guard.ts
// 'in' checks if property exists
type Circle = {
  kind: "circle";
  radius: number;
};

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

type Rectangle = {
  kind: "rectangle";
  width: number;
  height: number;
};

type Shape = Circle | Square | Rectangle;

function getArea(shape: Shape): number {
  if ("radius" in shape) {
    // TypeScript knows shape is Circle
    return Math.PI * shape.radius ** 2;
  }
  
  if ("size" in shape) {
    // TypeScript knows shape is Square
    return shape.size ** 2;
  }
  
  // TypeScript knows shape is Rectangle
  return shape.width * shape.height;
}

const circle: Circle = { kind: "circle", radius: 5 };
const square: Square = { kind: "square", size: 10 };
const rectangle: Rectangle = { kind: "rectangle", width: 20, height: 15 };

console.log(`Circle area: ${getArea(circle)}`);        // 78.54
console.log(`Square area: ${getArea(square)}`);        // 100
console.log(`Rectangle area: ${getArea(rectangle)}`);  // 300

// 'in' with optional properties
type User = {
  name: string;
  email: string;
  phone?: string;
};

function displayContact(user: User): void {
  console.log(`Name: ${user.name}`);
  console.log(`Email: ${user.email}`);
  
  if ("phone" in user && user.phone !== undefined) {
    console.log(`Phone: ${user.phone}`);
  }
}

displayContact({ name: "Yusuf", email: "yusuf@example.com" });
displayContact({ name: "Amina", email: "amina@example.com", phone: "+234-803-123-4567" });

// Multiple property checks
type ApiSuccess = {
  status: "success";
  data: any;
};

type ApiError = {
  status: "error";
  error: string;
};

type ApiResponse = ApiSuccess | ApiError;

function handleResponse(response: ApiResponse): void {
  if ("data" in response) {
    // TypeScript knows response is ApiSuccess
    console.log("Success:", response.data);
  } else {
    // TypeScript knows response is ApiError
    console.error("Error:", response.error);
  }
}

Equality Narrowing

equality-narrowing.ts
// Equality checks narrow types
function processStatus(status: "pending" | "approved" | "rejected"): string {
  if (status === "pending") {
    return "Waiting for approval";
  }
  
  if (status === "approved") {
    return "Request approved";
  }
  
  // TypeScript knows status is "rejected"
  return "Request rejected";
}

// Comparing with null/undefined
function greet(name: string | null | undefined): string {
  if (name === null) {
    return "Hello, guest (null)";
  }
  
  if (name === undefined) {
    return "Hello, guest (undefined)";
  }
  
  // TypeScript knows name is string
  return `Hello, ${name}!`;
}

console.log(greet("Yusuf"));      // "Hello, Yusuf!"
console.log(greet(null));         // "Hello, guest (null)"
console.log(greet(undefined));    // "Hello, guest (undefined)"

// Checking against specific values
type LoadingState = { state: "loading" };
type SuccessState = { state: "success"; data: string };
type ErrorState = { state: "error"; message: string };
type State = LoadingState | SuccessState | ErrorState;

function renderState(state: State): string {
  if (state.state === "loading") {
    return "Loading...";
  }
  
  if (state.state === "success") {
    // TypeScript knows state is SuccessState
    return `Success: ${state.data}`;
  }
  
  // TypeScript knows state is ErrorState
  return `Error: ${state.message}`;
}

console.log(renderState({ state: "loading" }));
console.log(renderState({ state: "success", data: "User data" }));
console.log(renderState({ state: "error", message: "Failed to load" }));

Custom Type Guards

Create your own type guard functions using the is keyword. These are reusable, testable, and provide clear type narrowing.

Basic Custom Type Guards

custom-guards-basic.ts
// Custom type guard with 'is' keyword
interface User {
  name: string;
  email: string;
}

interface Admin {
  name: string;
  email: string;
  role: string;
  permissions: string[];
}

// Type predicate function
function isAdmin(account: User | Admin): account is Admin {
  return "role" in account && "permissions" in account;
}

function displayAccount(account: User | Admin): void {
  console.log(`Name: ${account.name}`);
  console.log(`Email: ${account.email}`);
  
  if (isAdmin(account)) {
    // TypeScript knows account is Admin here
    console.log(`Role: ${account.role}`);
    console.log(`Permissions: ${account.permissions.join(", ")}`);
  } else {
    // TypeScript knows account is User here
    console.log("Regular user");
  }
}

const user: User = {
  name: "Yusuf",
  email: "yusuf@example.com"
};

const admin: Admin = {
  name: "Amina",
  email: "amina@example.com",
  role: "admin",
  permissions: ["read", "write", "delete"]
};

displayAccount(user);
displayAccount(admin);

// Type guard for arrays
function isStringArray(value: unknown): value is string[] {
  return (
    Array.isArray(value) &&
    value.every(item => typeof item === "string")
  );
}

function processArray(value: unknown): void {
  if (isStringArray(value)) {
    // TypeScript knows value is string[]
    console.log(`Strings: ${value.join(", ")}`);
    value.forEach(str => console.log(str.toUpperCase()));
  } else {
    console.log("Not a string array");
  }
}

processArray(["a", "b", "c"]);     // Works
processArray([1, 2, 3]);           // "Not a string array"
processArray("not an array");      // "Not a string array"

Complex Type Guards

complex-type-guards.ts
// Comprehensive type guard for API response
interface ApiSuccessResponse {
  status: "success";
  data: {
    id: number;
    name: string;
    email: string;
  };
  timestamp: string;
}

function isApiSuccessResponse(response: unknown): response is ApiSuccessResponse {
  if (typeof response !== "object" || response === null) {
    return false;
  }
  
  const obj = response as any;
  
  return (
    obj.status === "success" &&
    typeof obj.data === "object" &&
    obj.data !== null &&
    typeof obj.data.id === "number" &&
    typeof obj.data.name === "string" &&
    typeof obj.data.email === "string" &&
    typeof obj.timestamp === "string"
  );
}

function handleApiResponse(response: unknown): void {
  if (isApiSuccessResponse(response)) {
    // Safe to use all properties
    console.log(`User: ${response.data.name}`);
    console.log(`Email: ${response.data.email}`);
    console.log(`Time: ${response.timestamp}`);
  } else {
    console.error("Invalid API response");
  }
}

// Valid response
handleApiResponse({
  status: "success",
  data: { id: 1, name: "Yusuf", email: "yusuf@example.com" },
  timestamp: "2024-12-25T10:30:00Z"
});

// Invalid responses
handleApiResponse({ status: "error" });
handleApiResponse(null);
handleApiResponse("not an object");

// Type guard with generics
function isArrayOf<T>(
  value: unknown,
  itemGuard: (item: unknown) => item is T
): value is T[] {
  return Array.isArray(value) && value.every(itemGuard);
}

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

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

const data1 = [1, 2, 3, 4, 5];
const data2 = ["a", "b", "c"];
const data3 = [1, "two", 3];

console.log(isArrayOf(data1, isNumber));  // true
console.log(isArrayOf(data2, isString));  // true
console.log(isArrayOf(data3, isNumber));  // false

Reusable Type Guard Helpers

type-guard-helpers.ts
// Utility type guards
function isDefined<T>(value: T | undefined | null): value is T {
  return value !== undefined && value !== null;
}

function isNotNull<T>(value: T | null): value is T {
  return value !== null;
}

function hasProperty<K extends string>(
  obj: unknown,
  key: K
): obj is Record<K, unknown> {
  return typeof obj === "object" && obj !== null && key in obj;
}

// Using utility guards
const values = [1, undefined, 2, null, 3, undefined, 4];
const defined = values.filter(isDefined);
console.log(defined);  // [1, 2, 3, 4]

const nullable = [1, null, 2, null, 3];
const notNull = nullable.filter(isNotNull);
console.log(notNull);  // [1, 2, 3]

// Type guard for non-empty array
function isNonEmptyArray<T>(arr: T[]): arr is [T, ...T[]] {
  return arr.length > 0;
}

function processItems(items: string[]): void {
  if (isNonEmptyArray(items)) {
    // TypeScript knows first item exists
    const [first, ...rest] = items;
    console.log(`First: ${first}`);
    console.log(`Rest: ${rest.join(", ")}`);
  } else {
    console.log("Empty array");
  }
}

processItems(["a", "b", "c"]);  // "First: a" "Rest: b, c"
processItems([]);                // "Empty array"

// Type guard for specific object shape
interface Product {
  id: number;
  name: string;
  price: number;
}

function isProduct(value: unknown): value is Product {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "name" in value &&
    "price" in value &&
    typeof (value as any).id === "number" &&
    typeof (value as any).name === "string" &&
    typeof (value as any).price === "number"
  );
}

function processProduct(data: unknown): void {
  if (isProduct(data)) {
    console.log(`Product: ${data.name}, Price: ₦${data.price}`);
  } else {
    console.error("Invalid product data");
  }
}

processProduct({ id: 1, name: "Laptop", price: 450000 });  // Works
processProduct({ name: "Invalid" });                        // Error message

Assertion Functions

Assertion functions throw errors if a condition isn't met, and they narrow types for the rest of the scope.

assertion-functions.ts
// Assertion function with 'asserts' keyword
function assert(condition: boolean, message: string): asserts condition {
  if (!condition) {
    throw new Error(message);
  }
}

function processValue(value: string | null): void {
  assert(value !== null, "Value cannot be null");
  
  // TypeScript knows value is string here (not null)
  console.log(value.toUpperCase());
}

processValue("hello");  // "HELLO"
// processValue(null);  // Throws: "Value cannot be null"

// Type predicate assertion
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error(`Expected string, got ${typeof value}`);
  }
}

function processInput(input: unknown): void {
  assertIsString(input);
  
  // TypeScript knows input is string here
  console.log(input.toUpperCase());
}

processInput("hello");  // "HELLO"
// processInput(123);   // Throws: "Expected string, got number"

// Assertion for object shape
interface User {
  name: string;
  email: string;
}

function assertIsUser(value: unknown): asserts value is User {
  if (
    typeof value !== "object" ||
    value === null ||
    !("name" in value) ||
    !("email" in value) ||
    typeof (value as any).name !== "string" ||
    typeof (value as any).email !== "string"
  ) {
    throw new Error("Invalid user object");
  }
}

function registerUser(data: unknown): void {
  assertIsUser(data);
  
  // TypeScript knows data is User here
  console.log(`Registering ${data.name} with email ${data.email}`);
}

registerUser({ name: "Yusuf", email: "yusuf@example.com" });  // Works
// registerUser({ name: "Invalid" });  // Throws error

// Non-null assertion function
function assertDefined<T>(value: T | null | undefined): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error("Value is null or undefined");
  }
}

function getElement(id: string): HTMLElement | null {
  return document.getElementById(id);
}

function updateElement(id: string): void {
  const element = getElement(id);
  assertDefined(element);
  
  // TypeScript knows element is HTMLElement (not null)
  element.textContent = "Updated";
}

Type Assertions

Type assertions tell TypeScript to treat a value as a specific type. Use with caution—they bypass type checking and can lead to runtime errors.

as Keyword

as-assertions.ts
// Type assertion with 'as' keyword
let value: unknown = "hello";

// Assert that value is a string
let str = value as string;
console.log(str.toUpperCase());  // "HELLO"

// Without assertion, this would error
// console.log(value.toUpperCase());  // ❌ Error: unknown has no methods

// Asserting to more specific type
interface User {
  name: string;
  email: string;
}

let data: any = { name: "Yusuf", email: "yusuf@example.com" };
let user = data as User;
console.log(`User: ${user.name}`);

// Dangerous: Assertion without validation
let dangerous: unknown = { title: "Not a user" };
let fakeUser = dangerous as User;
// console.log(fakeUser.name.toUpperCase());  // Runtime error!

// Double assertion (escape hatch - very dangerous!)
let num = 42;
// let str = num as string;  // ❌ Error: number not assignable to string
let str2 = num as unknown as string;  // ✅ Compiles (but wrong!)
// console.log(str2.toUpperCase());  // Runtime error!

// Const assertion
let obj = {
  name: "Yusuf",
  age: 25
} as const;

// obj.name = "Amina";  // ❌ Error: readonly
// obj.age = 30;        // ❌ Error: readonly

// Type of obj is: { readonly name: "Yusuf"; readonly age: 25; }

// Array const assertion
let colors = ["red", "green", "blue"] as const;
// Type: readonly ["red", "green", "blue"]

// colors.push("yellow");  // ❌ Error: readonly
// colors[0] = "purple";   // ❌ Error: readonly

Non-null Assertion Operator

non-null-assertion.ts
// Non-null assertion with !
function getElement(id: string): HTMLElement | null {
  return document.getElementById(id);
}

// Without assertion
const element1 = getElement("myButton");
if (element1) {
  element1.textContent = "Click me";
}

// With non-null assertion (risky!)
const element2 = getElement("myButton")!;  // Assert it's not null
element2.textContent = "Click me";  // Might crash if element is null!

// Safer: Use optional chaining
const element3 = getElement("myButton");
element3?.addEventListener("click", () => {
  console.log("Clicked");
});

// Non-null assertion in object access
type Config = {
  host: string;
  port?: number;
};

const config: Config = { host: "localhost" };

// With assertion
const port1 = config.port!;  // Assert port exists (dangerous!)
console.log(port1 + 1000);   // Might be NaN if port is undefined!

// Without assertion (safer)
const port2 = config.port ?? 8080;
console.log(port2 + 1000);   // Always works

// Array access with non-null assertion
const users = ["Yusuf", "Amina", "Chidi"];

// Dangerous
const firstUser = users[0]!;  // Assert first element exists

// Safer
const secondUser = users[1];
if (secondUser) {
  console.log(secondUser.toUpperCase());
}

Warning: Type assertions and non-null assertions bypass TypeScript's safety checks. They don't perform any runtime validation. Only use them when you're absolutely certain about the type, and prefer type guards when possible.

Practical Examples

Example 1: Form Validation

form-validation.ts
// Form data validation with type guards
interface FormData {
  name: string;
  email: string;
  age: number;
  terms: boolean;
}

function isValidEmail(value: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(value);
}

function isFormData(data: unknown): data is FormData {
  if (typeof data !== "object" || data === null) {
    return false;
  }
  
  const obj = data as any;
  
  return (
    typeof obj.name === "string" &&
    obj.name.length >= 2 &&
    typeof obj.email === "string" &&
    isValidEmail(obj.email) &&
    typeof obj.age === "number" &&
    obj.age >= 18 &&
    obj.age <= 120 &&
    typeof obj.terms === "boolean" &&
    obj.terms === true
  );
}

function submitForm(data: unknown): void {
  if (isFormData(data)) {
    console.log("Form submitted successfully!");
    console.log(`Name: ${data.name}`);
    console.log(`Email: ${data.email}`);
    console.log(`Age: ${data.age}`);
  } else {
    console.error("Invalid form data");
  }
}

// Valid submission
submitForm({
  name: "Yusuf Ibrahim",
  email: "yusuf@example.com",
  age: 25,
  terms: true
});

// Invalid submissions
submitForm({ name: "A", email: "invalid", age: 15, terms: false });
submitForm({ name: "Yusuf" });  // Missing fields

Example 2: API Response Handler

api-response-handler.ts
// Type-safe API response handling
type ApiSuccess<T> = {
  status: 200 | 201;
  data: T;
};

type ApiError = {
  status: 400 | 401 | 404 | 500;
  error: {
    code: string;
    message: string;
  };
};

type ApiResponse<T> = ApiSuccess<T> | ApiError;

function isApiSuccess<T>(response: ApiResponse<T>): response is ApiSuccess<T> {
  return response.status >= 200 && response.status < 300;
}

function isApiError<T>(response: ApiResponse<T>): response is ApiError {
  return response.status >= 400;
}

interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUser(id: number): Promise<User> {
  // Simulate API call
  const response: ApiResponse<User> = {
    status: 200,
    data: { id, name: "Yusuf Ibrahim", email: "yusuf@example.com" }
  };
  
  if (isApiSuccess(response)) {
    // TypeScript knows response has 'data'
    return response.data;
  }
  
  if (isApiError(response)) {
    // TypeScript knows response has 'error'
    throw new Error(`API Error: ${response.error.message}`);
  }
  
  throw new Error("Unexpected response");
}

// Usage
fetchUser(1)
  .then(user => console.log(`User: ${user.name}`))
  .catch(error => console.error(error.message));

Example 3: Safe JSON Parsing

safe-json-parsing.ts
// Type-safe JSON parsing
interface Product {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
}

function isProduct(value: unknown): value is Product {
  if (typeof value !== "object" || value === null) {
    return false;
  }
  
  const obj = value as any;
  
  return (
    typeof obj.id === "number" &&
    typeof obj.name === "string" &&
    typeof obj.price === "number" &&
    obj.price > 0 &&
    typeof obj.inStock === "boolean"
  );
}

function parseProductJSON(json: string): Product {
  let parsed: unknown;
  
  try {
    parsed = JSON.parse(json);
  } catch (error) {
    throw new Error("Invalid JSON");
  }
  
  if (isProduct(parsed)) {
    return parsed;
  }
  
  throw new Error("Invalid product data");
}

// Valid JSON
const validJSON = '{"id":1,"name":"Laptop","price":450000,"inStock":true}';
const product = parseProductJSON(validJSON);
console.log(`Product: ${product.name}, Price: ₦${product.price}`);

// Invalid JSON
try {
  const invalidJSON = '{"id":"not a number","name":"Laptop"}';
  parseProductJSON(invalidJSON);
} catch (error) {
  console.error((error as Error).message);  // "Invalid product data"
}

// Array of products
function isProductArray(value: unknown): value is Product[] {
  return Array.isArray(value) && value.every(isProduct);
}

function parseProductArrayJSON(json: string): Product[] {
  const parsed = JSON.parse(json);
  
  if (isProductArray(parsed)) {
    return parsed;
  }
  
  throw new Error("Invalid product array");
}

const arrayJSON = '[{"id":1,"name":"Laptop","price":450000,"inStock":true}]';
const products = parseProductArrayJSON(arrayJSON);
console.log(`Products: ${products.length}`);

Test Your Knowledge

Type Safety Check

Which type guard pattern is safest?

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

function processUser(user: User | Admin) {
  // Option A
  console.log((user as Admin).role);

  // Option B
  if ("role" in user) {
    console.log(user.role);
  }

  // Option C
  function isAdmin(user: User | Admin): user is Admin {
    return "role" in user;
  }
  if (isAdmin(user)) {
    console.log(user.role);
  }

  // Option D
  console.log(user.role);
}

Common TypeScript Error

Code with Error
// Unsafe type assertion
interface User {
  name: string;
  email: string;
}

function processData(data: unknown) {
  // Unsafe assertion - no runtime check!
  const user = data as User;
  
  // This might crash if data doesn't match User shape
  console.log(user.name.toUpperCase());
  console.log(user.email.toLowerCase());
}

// Calling with wrong data
processData({ title: "Not a user" });  // Runtime crash!
processData(null);                      // Runtime crash!

❌ TypeError: Cannot read property 'toUpperCase' of undefined (runtime error)

What's Wrong?

Type assertions bypass TypeScript's type checking. They tell TypeScript 'trust me, I know what I'm doing' but provide no runtime safety. If the data doesn't match, you get runtime errors.

Corrected Code
// Safe approach with type guard
interface User {
  name: string;
  email: string;
}

function isUser(data: unknown): data is User {
  return (
    typeof data === "object" &&
    data !== null &&
    "name" in data &&
    "email" in data &&
    typeof (data as any).name === "string" &&
    typeof (data as any).email === "string"
  );
}

function processData(data: unknown) {
  if (isUser(data)) {
    // Safe to use - type guard validated the data
    console.log(data.name.toUpperCase());
    console.log(data.email.toLowerCase());
  } else {
    console.error("Invalid data: not a User");
  }
}

// Now safe with any input
processData({ name: "Yusuf", email: "yusuf@example.com" });  // ✅ Works
processData({ title: "Not a user" });  // ✅ Handles gracefully
processData(null);                      // ✅ Handles gracefully

Solution: Use type guards to validate data before using it

Best Practices

  1. Prefer type guards over assertions - safer and more maintainable
  2. Create reusable type guard functions for common patterns
  3. Validate unknown data from APIs, user input, or external sources
  4. Use assertion functions for precondition checks
  5. Avoid non-null assertions (!) unless absolutely necessary
  6. Document why you use type assertions when you must
  7. Test type guards with valid and invalid data
  8. Use const assertions for literal types
  9. Combine guards for complex validation
  10. Use optional chaining instead of non-null assertions
best-practices-example.ts
// ✅ Good: Type guard with validation
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "name" in value &&
    "email" in value &&
    typeof (value as any).name === "string" &&
    typeof (value as any).email === "string"
  );
}

function processUser(data: unknown): void {
  if (isUser(data)) {
    console.log(data.name);  // Safe
  }
}

// ❌ Avoid: Assertion without validation
function badProcessUser(data: unknown): void {
  const user = data as User;  // Unsafe!
  console.log(user.name);     // Might crash
}

// ✅ Good: Assertion function for preconditions
function assertPositive(value: number): asserts value is number {
  if (value <= 0) {
    throw new Error("Value must be positive");
  }
}

function calculateDiscount(price: number, percent: number): number {
  assertPositive(price);
  assertPositive(percent);
  return price * (percent / 100);
}

// ❌ Avoid: Non-null assertion
const element = document.getElementById("btn")!;  // Risky
element.click();  // Might crash

// ✅ Better: Optional chaining
const safeElement = document.getElementById("btn");
safeElement?.click();  // Safe

// ✅ Good: Const assertion for literals
const config = {
  host: "localhost",
  port: 8080,
  secure: false
} as const;

// ❌ Avoid: Double assertion (very dangerous)
const num = 42;
const str = num as unknown as string;  // Don't do this!

Key Takeaways

  • Type guards safely narrow types with runtime checks
  • Built-in guards: typeof, instanceof, in, equality checks
  • Custom type guards use is keyword for reusable validation
  • Assertion functions use asserts to narrow types after validation
  • Type assertions (as) bypass safety checks—use with caution
  • Non-null assertion (!) assumes value isn't null—dangerous
  • Always validate unknown data from external sources
  • Prefer type guards over assertions for maintainable code
  • Use const assertions (as const) for literal types
  • Optional chaining (?.) is safer than non-null assertions

What's Next?

Excellent! You've mastered type guards and type assertions, completing your understanding of safe type narrowing. Next, we'll explore Literal Types and Type Narrowing in even more depth. You'll learn about string literals, numeric literals, boolean literals, template literal types, and advanced narrowing patterns. These features give you incredible precision in your type definitions!

Get ready to work with ultra-precise types!

Mastering TypeScript type guards and assertions! Check this out!

Previous
Union and Intersection Types
Next
Literal Types

Never Miss a New TypeScript Tutorial

Join 1,000+ developers learning TypeScript step-by-step. Get new tutorials, type safety tips, and exclusive resources delivered to your inbox - completely FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

TypeScript Tutorials

0 of 30 completed

Your Progress0%

Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

© 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo