+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 21 of 354

๐Ÿ›ก ๏ธ Type Assertions and Type Guards: Narrowing Types Safely

Master type assertions and type guards in TypeScript to handle unknown types safely and write bulletproof code ๐Ÿš€

๐Ÿš€Intermediate
30 min read

Prerequisites

  • Strong TypeScript fundamentals ๐Ÿ“
  • Understanding of union types โšก
  • Basic knowledge of type system ๐Ÿ’ป

What you'll learn

  • Use type assertions safely and correctly ๐ŸŽฏ
  • Create custom type guards ๐Ÿ—๏ธ
  • Narrow types with built-in guards ๐Ÿ”
  • Handle unknown data confidently โœจ

๐ŸŽฏ Introduction

Welcome to the defensive programming world of type assertions and type guards! ๐ŸŽ‰ In this guide, weโ€™ll explore how to handle uncertain types safely and tell TypeScript โ€œtrust me, I know what Iโ€™m doingโ€ - responsibly.

Think of type assertions as your โ€œI know betterโ€ card ๐ŸŽด and type guards as your security checkpoints ๐Ÿšฌ. Together, they help you navigate the sometimes murky waters between what TypeScript thinks and what you know to be true.

By the end of this tutorial, youโ€™ll be narrowing types like a pro, handling any data that comes your way with confidence and safety! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Type Assertions and Type Guards

๐Ÿค” What Are Type Assertions?

Type assertions are like telling TypeScript โ€œI know more than you doโ€ ๐Ÿง. Theyโ€™re a way to override TypeScriptโ€™s inferred type when you have more information about the valueโ€™s type.

Think of it as a security override ๐Ÿ”“ - use with caution!

// TypeScript thinks it's unknown
const data: unknown = "Hello";
// You assert it's a string
const message = data as string;

๐Ÿ’ก What Are Type Guards?

Type guards are like bouncers at a club ๐Ÿšช - they check if something meets certain criteria before letting it through. They narrow down types within conditional blocks.

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

๐ŸŽฏ Why Use Them?

Hereโ€™s why these tools are essential:

  1. Handle External Data ๐ŸŒ: Safely process API responses
  2. Type Narrowing ๐ŸŽฏ: Eliminate union type ambiguity
  3. Runtime Safety ๐Ÿ›ก๏ธ: Validate types at runtime
  4. Better IntelliSense ๐Ÿ’ป: Help TypeScript help you
  5. Defensive Programming ๐Ÿฐ: Guard against unexpected inputs

Real-world example: Processing user input from a form ๐Ÿ“ - you need to verify itโ€™s the right type before using it!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Type Assertions Basics

Letโ€™s explore type assertions:

// ๐ŸŽจ Two syntaxes for type assertions

// Angle bracket syntax (doesn't work in JSX)
const value1 = <string>someValue;

// 'as' syntax (works everywhere - preferred!)
const value2 = someValue as string;

// ๐Ÿ‘‹ Real example
const element = document.getElementById('myButton');
// TypeScript thinks it might be null
const button = element as HTMLButtonElement;
button.disabled = true; // Now we can use button properties!

// ๐ŸŽฏ Asserting to more specific types
interface User {
  id: number;
  name: string;
  role: "admin" | "user";
}

// API returns unknown data
const userData: unknown = {
  id: 1,
  name: "Alice",
  role: "admin"
};

// Assert it's a User
const user = userData as User;
console.log(`๐Ÿ‘ค ${user.name} is an ${user.role}`);

// โš ๏ธ Const assertions - super specific!
const config = {
  endpoint: "https://api.example.com",
  timeout: 5000
} as const;
// Now config.endpoint is "https://api.example.com", not string!

// ๐ŸŽจ Type assertion with literals
const eventType = "click" as "click" | "focus" | "blur";

๐ŸŽฏ Type Guards Basics

Now letโ€™s master type guards:

// ๐Ÿ›ก๏ธ Built-in type guards

// typeof guard
function processValue(value: string | number) {
  if (typeof value === "string") {
    // TypeScript knows it's string
    console.log(`String length: ${value.length}`);
  } else {
    // TypeScript knows it's number
    console.log(`Number squared: ${value ** 2}`);
  }
}

// instanceof guard
class Cat {
  meow() { console.log("๐Ÿฑ Meow!"); }
}

class Dog {
  bark() { console.log("๐Ÿถ Woof!"); }
}

function petSound(pet: Cat | Dog) {
  if (pet instanceof Cat) {
    pet.meow();
  } else {
    pet.bark();
  }
}

// ๐ŸŽฏ in operator guard
interface Bird {
  fly(): void;
  wings: number;
}

interface Fish {
  swim(): void;
  fins: number;
}

function move(animal: Bird | Fish) {
  if ("wings" in animal) {
    console.log(`๐Ÿฆ… Flying with ${animal.wings} wings`);
    animal.fly();
  } else {
    console.log(`๐ŸŸ Swimming with ${animal.fins} fins`);
    animal.swim();
  }
}

// ๐Ÿ” Custom type guards
function isString(value: unknown): value is string {
  return typeof value === "string";
}

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

// Usage
const data: unknown = "Hello";
if (isString(data)) {
  console.log(data.toUpperCase()); // TypeScript knows it's string!
}

๐Ÿ”„ Combining Assertions and Guards

// ๐Ÿ—๏ธ Safe assertion pattern
function getElement<T extends HTMLElement>(
  id: string,
  type: new () => T
): T | null {
  const element = document.getElementById(id);
  
  if (element instanceof type) {
    return element;
  }
  
  console.warn(`โš ๏ธ Element ${id} is not a ${type.name}`);
  return null;
}

// Usage
const button = getElement("submit-btn", HTMLButtonElement);
if (button) {
  button.onclick = () => console.log("๐Ÿ”˜ Clicked!");
}

// ๐ŸŽจ Discriminated unions with guards
type Result<T> = 
  | { success: true; data: T }
  | { success: false; error: Error };

function isSuccess<T>(result: Result<T>): result is { success: true; data: T } {
  return result.success === true;
}

function processResult<T>(result: Result<T>) {
  if (isSuccess(result)) {
    console.log("โœ… Data:", result.data);
  } else {
    console.log("โŒ Error:", result.error.message);
  }
}

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: API Response Handler

Letโ€™s build a robust API response handler:

// ๐ŸŒ API response types
interface ApiUser {
  id: number;
  name: string;
  email: string;
  role: "admin" | "user" | "guest";
  metadata?: Record<string, any>;
}

interface ApiProduct {
  id: string;
  title: string;
  price: number;
  inStock: boolean;
  categories: string[];
}

interface ApiError {
  code: string;
  message: string;
  details?: unknown;
}

// ๐Ÿ›ก๏ธ Type guards for API responses
function isApiError(data: unknown): data is ApiError {
  return (
    typeof data === "object" &&
    data !== null &&
    "code" in data &&
    "message" in data &&
    typeof (data as ApiError).code === "string" &&
    typeof (data as ApiError).message === "string"
  );
}

function isApiUser(data: unknown): data is ApiUser {
  if (typeof data !== "object" || data === null) return false;
  
  const user = data as ApiUser;
  return (
    typeof user.id === "number" &&
    typeof user.name === "string" &&
    typeof user.email === "string" &&
    ["admin", "user", "guest"].includes(user.role)
  );
}

function isApiProduct(data: unknown): data is ApiProduct {
  if (typeof data !== "object" || data === null) return false;
  
  const product = data as ApiProduct;
  return (
    typeof product.id === "string" &&
    typeof product.title === "string" &&
    typeof product.price === "number" &&
    typeof product.inStock === "boolean" &&
    Array.isArray(product.categories)
  );
}

// ๐ŸŽฏ API client with type-safe responses
class ApiClient {
  private baseUrl = "https://api.example.com";
  
  async fetchUser(id: number): Promise<ApiUser> {
    const response = await fetch(`${this.baseUrl}/users/${id}`);
    const data: unknown = await response.json();
    
    if (isApiError(data)) {
      console.error(`โŒ API Error: ${data.message}`);
      throw new Error(data.message);
    }
    
    if (!isApiUser(data)) {
      console.error("โš ๏ธ Invalid user data received");
      throw new Error("Invalid user data format");
    }
    
    console.log(`โœ… User fetched: ${data.name}`);
    return data;
  }
  
  async fetchProducts(): Promise<ApiProduct[]> {
    const response = await fetch(`${this.baseUrl}/products`);
    const data: unknown = await response.json();
    
    if (!Array.isArray(data)) {
      throw new Error("Expected array of products");
    }
    
    // Filter and validate each product
    const validProducts = data.filter((item): item is ApiProduct => {
      if (!isApiProduct(item)) {
        console.warn("โš ๏ธ Skipping invalid product:", item);
        return false;
      }
      return true;
    });
    
    console.log(`๐Ÿ“ฆ Fetched ${validProducts.length} valid products`);
    return validProducts;
  }
}

// ๐ŸŽฎ Usage
const api = new ApiClient();

// Fetch user with type safety
api.fetchUser(1)
  .then(user => {
    console.log(`๐Ÿ‘ค ${user.name} (${user.role})`);
    // TypeScript knows all user properties!
  })
  .catch(error => {
    console.error("๐Ÿšจ Failed to fetch user:", error);
  });

๐ŸŽจ Example 2: Form Validation System

Letโ€™s create a type-safe form validator:

// ๐Ÿ“ Form field types
type FormValue = string | number | boolean | Date | File | null;

interface FormField {
  name: string;
  value: FormValue;
  type: "text" | "number" | "email" | "date" | "file" | "checkbox";
  required: boolean;
  validators?: Array<(value: FormValue) => string | null>;
}

// ๐Ÿ” Type guards for form values
function isNonEmpty(value: FormValue): value is string | number | Date | File {
  return value !== null && value !== "" && value !== undefined;
}

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

function isNumeric(value: unknown): value is number {
  return typeof value === "number" && !isNaN(value);
}

function isDateValue(value: unknown): value is Date {
  return value instanceof Date && !isNaN(value.getTime());
}

// ๐ŸŽฏ Form validator class
class FormValidator {
  private errors = new Map<string, string[]>();
  
  validateField(field: FormField): boolean {
    const fieldErrors: string[] = [];
    
    // Required validation
    if (field.required && !isNonEmpty(field.value)) {
      fieldErrors.push(`${field.name} is required`);
    }
    
    // Type-specific validation
    switch (field.type) {
      case "email":
        if (isNonEmpty(field.value) && !isValidEmail(field.value)) {
          fieldErrors.push("Invalid email format");
        }
        break;
        
      case "number":
        if (isNonEmpty(field.value) && !isNumeric(field.value)) {
          fieldErrors.push("Must be a valid number");
        }
        break;
        
      case "date":
        if (isNonEmpty(field.value) && !isDateValue(field.value)) {
          fieldErrors.push("Invalid date");
        }
        break;
        
      case "file":
        if (field.value && !(field.value instanceof File)) {
          fieldErrors.push("Invalid file");
        }
        break;
    }
    
    // Custom validators
    if (field.validators && isNonEmpty(field.value)) {
      field.validators.forEach(validator => {
        const error = validator(field.value);
        if (error) fieldErrors.push(error);
      });
    }
    
    // Store errors
    if (fieldErrors.length > 0) {
      this.errors.set(field.name, fieldErrors);
      return false;
    } else {
      this.errors.delete(field.name);
      return true;
    }
  }
  
  validateForm(fields: FormField[]): boolean {
    let isValid = true;
    
    fields.forEach(field => {
      if (!this.validateField(field)) {
        isValid = false;
      }
    });
    
    return isValid;
  }
  
  getErrors(): Record<string, string[]> {
    return Object.fromEntries(this.errors);
  }
  
  displayErrors(): void {
    if (this.errors.size === 0) {
      console.log("โœ… Form is valid!");
      return;
    }
    
    console.log("โŒ Form validation errors:");
    this.errors.forEach((errors, field) => {
      console.log(`\n๐Ÿ“‹ ${field}:`);
      errors.forEach(error => console.log(`  - ${error}`));
    });
  }
}

// ๐ŸŽฎ Test the form validator
const validator = new FormValidator();

const formFields: FormField[] = [
  {
    name: "Email",
    value: "invalid-email",
    type: "email",
    required: true
  },
  {
    name: "Age",
    value: "not a number" as any,
    type: "number",
    required: true
  },
  {
    name: "Terms",
    value: true,
    type: "checkbox",
    required: true,
    validators: [
      (value) => value === true ? null : "You must accept the terms"
    ]
  },
  {
    name: "BirthDate",
    value: new Date("2000-01-01"),
    type: "date",
    required: false,
    validators: [
      (value) => {
        if (isDateValue(value)) {
          const age = new Date().getFullYear() - value.getFullYear();
          return age >= 18 ? null : "Must be 18 or older";
        }
        return null;
      }
    ]
  }
];

const isValid = validator.validateForm(formFields);
validator.displayErrors();

๐ŸŽฎ Example 3: Dynamic Component System

// ๐ŸŽจ Component types
interface ButtonComponent {
  type: "button";
  text: string;
  onClick: () => void;
  variant?: "primary" | "secondary" | "danger";
}

interface InputComponent {
  type: "input";
  placeholder: string;
  value: string;
  onChange: (value: string) => void;
  inputType?: "text" | "password" | "email";
}

interface SelectComponent {
  type: "select";
  options: Array<{ value: string; label: string }>;
  value: string;
  onChange: (value: string) => void;
}

type DynamicComponent = ButtonComponent | InputComponent | SelectComponent;

// ๐Ÿ›ก๏ธ Component type guards
function isButton(component: DynamicComponent): component is ButtonComponent {
  return component.type === "button";
}

function isInput(component: DynamicComponent): component is InputComponent {
  return component.type === "input";
}

function isSelect(component: DynamicComponent): component is SelectComponent {
  return component.type === "select";
}

// ๐ŸŽฏ Component renderer
class ComponentRenderer {
  render(component: DynamicComponent): void {
    console.log(`\n๐ŸŽจ Rendering ${component.type} component:`);
    
    if (isButton(component)) {
      this.renderButton(component);
    } else if (isInput(component)) {
      this.renderInput(component);
    } else if (isSelect(component)) {
      this.renderSelect(component);
    } else {
      // Exhaustive check
      const _never: never = component;
      throw new Error(`Unknown component type: ${_never}`);
    }
  }
  
  private renderButton(button: ButtonComponent): void {
    const variant = button.variant || "primary";
    const emoji = {
      primary: "๐Ÿ”ต",
      secondary: "โšช",
      danger: "๐Ÿ”ด"
    }[variant];
    
    console.log(`${emoji} [${button.text}] (${variant} button)`);
    console.log(`   Click handler attached ๐Ÿ”˜`);
  }
  
  private renderInput(input: InputComponent): void {
    const type = input.inputType || "text";
    const emoji = {
      text: "๐Ÿ“",
      password: "๐Ÿ”’",
      email: "๐Ÿ“ง"
    }[type];
    
    console.log(`${emoji} Input (${type})`);
    console.log(`   Placeholder: "${input.placeholder}"`);
    console.log(`   Value: "${input.value}"`);
  }
  
  private renderSelect(select: SelectComponent): void {
    console.log(`๐Ÿ“œ Select dropdown`);
    console.log(`   Current: "${select.value}"`);
    console.log(`   Options:`);
    select.options.forEach(opt => {
      const check = opt.value === select.value ? "โœ…" : "โฌœ";
      console.log(`     ${check} ${opt.label} (${opt.value})`);
    });
  }
}

// ๐ŸŽฎ Test the renderer
const renderer = new ComponentRenderer();

const components: DynamicComponent[] = [
  {
    type: "button",
    text: "Submit Form",
    onClick: () => console.log("Form submitted!"),
    variant: "primary"
  },
  {
    type: "input",
    placeholder: "Enter your email",
    value: "[email protected]",
    onChange: (v) => console.log(`Email changed: ${v}`),
    inputType: "email"
  },
  {
    type: "select",
    options: [
      { value: "ts", label: "TypeScript" },
      { value: "js", label: "JavaScript" },
      { value: "py", label: "Python" }
    ],
    value: "ts",
    onChange: (v) => console.log(`Selected: ${v}`)
  }
];

components.forEach(component => renderer.render(component));

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Advanced Type Guard Patterns

Level up with sophisticated type guards:

// ๐ŸŽฏ Generic type guard factory
function createTypeGuard<T>(
  validator: (value: unknown) => boolean
): (value: unknown) => value is T {
  return (value: unknown): value is T => validator(value);
}

// ๐ŸŽจ Schema-based validation
interface Schema {
  [key: string]: "string" | "number" | "boolean" | Schema;
}

function validateSchema<T>(data: unknown, schema: Schema): data is T {
  if (typeof data !== "object" || data === null) return false;
  
  for (const [key, type] of Object.entries(schema)) {
    const value = (data as any)[key];
    
    if (typeof type === "object") {
      if (!validateSchema(value, type)) return false;
    } else if (typeof value !== type) {
      return false;
    }
  }
  
  return true;
}

// Usage
interface User {
  name: string;
  age: number;
  preferences: {
    theme: string;
    notifications: boolean;
  };
}

const userSchema: Schema = {
  name: "string",
  age: "number",
  preferences: {
    theme: "string",
    notifications: "boolean"
  }
};

const data: unknown = {
  name: "Alice",
  age: 30,
  preferences: {
    theme: "dark",
    notifications: true
  }
};

if (validateSchema<User>(data, userSchema)) {
  console.log(`๐Ÿ‘ค Valid user: ${data.name}`);
}

// ๐Ÿš€ Branded types for extra safety
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<number, "UserId">;
type ProductId = Brand<string, "ProductId">;

function isUserId(value: unknown): value is UserId {
  return typeof value === "number" && value > 0;
}

function isProductId(value: unknown): value is ProductId {
  return typeof value === "string" && value.startsWith("PROD-");
}

// Type-safe ID usage
function getUser(id: UserId): void {
  console.log(`Fetching user ${id}`);
}

const maybeUserId: unknown = 123;
if (isUserId(maybeUserId)) {
  getUser(maybeUserId); // Type safe!
}

๐Ÿ—๏ธ Assertion Functions

TypeScript 3.7+ assertion functions:

// ๐Ÿ›ก๏ธ Assertion functions narrow types
function assertString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error(`Expected string, got ${typeof value}`);
  }
}

function assertNonNull<T>(value: T | null | undefined): asserts value is T {
  if (value == null) {
    throw new Error("Value cannot be null or undefined");
  }
}

function assertUserRole(
  user: { role: string },
  allowedRoles: readonly string[]
): asserts user is { role: typeof allowedRoles[number] } {
  if (!allowedRoles.includes(user.role)) {
    throw new Error(`Invalid role: ${user.role}`);
  }
}

// ๐ŸŽฎ Usage - type narrowing after assertion
function processData(data: unknown) {
  assertString(data);
  // TypeScript knows data is string now!
  console.log(data.toUpperCase());
}

function processUser(user: { role: string } | null) {
  assertNonNull(user);
  assertUserRole(user, ["admin", "moderator"] as const);
  // TypeScript knows user.role is "admin" | "moderator"
  
  if (user.role === "admin") {
    console.log("๐Ÿ‘‘ Admin privileges granted");
  }
}

// ๐ŸŽจ Custom assertion with detailed errors
class ValidationError extends Error {
  constructor(
    public field: string,
    public expected: string,
    public actual: unknown
  ) {
    super(`${field}: expected ${expected}, got ${typeof actual}`);
  }
}

function assertValidEmail(value: unknown): asserts value is string {
  assertString(value);
  
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(value)) {
    throw new ValidationError("email", "valid email format", value);
  }
}

๐ŸŽจ Template Literal Type Guards

// ๐Ÿงฌ Advanced string pattern matching
type HexColor = `#${string}`;
type RGBColor = `rgb(${number}, ${number}, ${number})`;
type Color = HexColor | RGBColor | "transparent";

function isHexColor(value: string): value is HexColor {
  return /^#[0-9A-Fa-f]{6}$/.test(value);
}

function isRGBColor(value: string): value is RGBColor {
  return /^rgb\(\d{1,3}, \d{1,3}, \d{1,3}\)$/.test(value);
}

function parseColor(color: string): Color | null {
  if (isHexColor(color)) {
    console.log(`๐ŸŽจ Hex color: ${color}`);
    return color;
  } else if (isRGBColor(color)) {
    console.log(`๐ŸŒˆ RGB color: ${color}`);
    return color;
  } else if (color === "transparent") {
    console.log(`๐Ÿ‘ป Transparent`);
    return color;
  }
  
  console.error(`โŒ Invalid color: ${color}`);
  return null;
}

// ๐ŸŽฏ URL pattern matching
type HTTPUrl = `http://${string}`;
type HTTPSUrl = `https://${string}`;
type URL = HTTPUrl | HTTPSUrl;

function isSecureUrl(url: string): url is HTTPSUrl {
  return url.startsWith("https://");
}

function fetchSecure(url: URL): void {
  if (isSecureUrl(url)) {
    console.log(`๐Ÿ”’ Secure fetch: ${url}`);
  } else {
    console.warn(`โš ๏ธ Insecure URL: ${url}`);
  }
}

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Unsafe Type Assertions

// โŒ Wrong - lying to TypeScript!
const data: unknown = { name: "Alice" };
const user = data as { id: number; email: string };
console.log(user.id); // undefined at runtime! ๐Ÿ’ฅ

// โœ… Correct - validate before asserting
if (isValidUser(data)) {
  console.log(user.id); // Safe!
}

// ๐ŸŽฏ Better - use type guards
function getUser(data: unknown): User | null {
  if (isUser(data)) {
    return data;
  }
  return null;
}

๐Ÿคฏ Pitfall 2: Double Assertions

// โŒ Very dangerous - bypassing all safety!
const value = "hello" as unknown as number;
// TypeScript thinks it's a number, but it's a string!

// โœ… If you must assert, validate first
function toNumber(value: unknown): number {
  const num = Number(value);
  if (isNaN(num)) {
    throw new Error(`Cannot convert ${value} to number`);
  }
  return num;
}

๐Ÿ˜ต Pitfall 3: Incomplete Type Guards

// โŒ Incomplete guard - missing properties
function badIsUser(value: unknown): value is User {
  return typeof value === "object" && value !== null;
  // Forgot to check for required properties!
}

// โœ… Complete guard - check all properties
function goodIsUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "name" in value &&
    "email" in value &&
    typeof (value as User).id === "number" &&
    typeof (value as User).name === "string" &&
    typeof (value as User).email === "string"
  );
}

๐Ÿค” Pitfall 4: Type Guard Logic Errors

// โŒ Wrong logic in type guard
function isStringOrNumber(value: unknown): value is string | number {
  // This is AND, not OR!
  return typeof value === "string" && typeof value === "number";
  // Always returns false!
}

// โœ… Correct logic
function isStringOrNumber(value: unknown): value is string | number {
  return typeof value === "string" || typeof value === "number";
}

// ๐ŸŽฏ Even better - separate guards
function processValue(value: unknown) {
  if (typeof value === "string") {
    console.log("String:", value.toUpperCase());
  } else if (typeof value === "number") {
    console.log("Number:", value.toFixed(2));
  }
}

๐Ÿ˜ฌ Pitfall 5: Forgetting About Arrays

// โŒ Forgetting arrays are objects too
function isObject(value: unknown): value is object {
  return typeof value === "object" && value !== null;
  // Arrays will pass this check!
}

const arr = [1, 2, 3];
if (isObject(arr)) {
  // arr is treated as object, not array!
}

// โœ… Distinguish between arrays and objects
function isPlainObject(value: unknown): value is Record<string, unknown> {
  return (
    typeof value === "object" &&
    value !== null &&
    !Array.isArray(value) &&
    !(value instanceof Date) &&
    !(value instanceof RegExp)
  );
}

// ๐ŸŽฏ Specific checks
if (Array.isArray(value)) {
  console.log("๐Ÿ“‹ It's an array");
} else if (isPlainObject(value)) {
  console.log("๐Ÿ“ฆ It's a plain object");
}

๐Ÿ› ๏ธ Best Practices

1. ๐ŸŽฏ Prefer Type Guards Over Assertions

// โœ… Good - runtime validation
function processData(data: unknown) {
  if (isUser(data)) {
    console.log(data.name); // Safe!
  }
}

// โŒ Avoid - no runtime check
function processDataBad(data: unknown) {
  const user = data as User; // Dangerous!
  console.log(user.name);
}

2. ๐Ÿ“ Create Reusable Type Guards

// โœ… Good - reusable validation
const typeGuards = {
  isString: (value: unknown): value is string => 
    typeof value === "string",
  
  isNumber: (value: unknown): value is number => 
    typeof value === "number" && !isNaN(value),
  
  isNonEmptyString: (value: unknown): value is string => 
    typeof value === "string" && value.length > 0,
  
  isPositiveNumber: (value: unknown): value is number => 
    typeof value === "number" && value > 0,
  
  isEmail: (value: unknown): value is string => 
    typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
};

3. ๐Ÿ›ก๏ธ Use Assertion Functions for Critical Paths

// โœ… Good - fail fast for critical errors
function assertDefined<T>(
  value: T | null | undefined,
  message?: string
): asserts value is T {
  if (value == null) {
    throw new Error(message || "Value must be defined");
  }
}

// Usage
const config = getConfig();
assertDefined(config, "Configuration is required");
// TypeScript knows config is defined here

4. ๐ŸŽจ Combine Guards for Complex Types

// โœ… Good - compose simple guards
function isUserWithPremium(value: unknown): value is User & { premium: true } {
  return isUser(value) && 
         "premium" in value && 
         value.premium === true;
}

function hasRequiredFields<T extends object>(
  obj: unknown,
  fields: (keyof T)[]
): obj is T {
  if (typeof obj !== "object" || obj === null) return false;
  
  return fields.every(field => field in obj);
}

5. โœจ Document Your Assertions

// โœ… Good - clear documentation
/**
 * Asserts that a value is a valid User object.
 * @throws {ValidationError} If validation fails
 * @example
 * assertUser(data); // Throws if invalid
 * console.log(data.name); // Safe to use
 */
function assertUser(value: unknown): asserts value is User {
  if (!isUser(value)) {
    throw new ValidationError("User", value);
  }
}

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Type-Safe JSON Parser

Create a JSON parser with full type safety and validation:

๐Ÿ“‹ Requirements:

  • โœ… Parse JSON strings with type validation
  • ๐Ÿท๏ธ Support nested objects and arrays
  • ๐Ÿ‘ค Custom type guards for data shapes
  • ๐Ÿ“… Handle dates and special types
  • ๐ŸŽจ Detailed error reporting

๐Ÿš€ Bonus Points:

  • Add schema validation
  • Implement transformation pipelines
  • Create type inference from schemas

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ Type-safe JSON parser with validation

// Schema definition types
type SchemaType = 
  | "string" 
  | "number" 
  | "boolean" 
  | "date"
  | "array"
  | "object"
  | "null";

interface SchemaNode {
  type: SchemaType;
  optional?: boolean;
  items?: SchemaNode; // For arrays
  properties?: Record<string, SchemaNode>; // For objects
  validate?: (value: any) => boolean;
  transform?: (value: any) => any;
}

// Type guards for primitives
const primitiveGuards = {
  string: (v: unknown): v is string => typeof v === "string",
  number: (v: unknown): v is number => typeof v === "number" && !isNaN(v),
  boolean: (v: unknown): v is boolean => typeof v === "boolean",
  null: (v: unknown): v is null => v === null,
  date: (v: unknown): v is Date => {
    if (v instanceof Date) return !isNaN(v.getTime());
    if (typeof v === "string") {
      const date = new Date(v);
      return !isNaN(date.getTime());
    }
    return false;
  }
};

// ๐Ÿ›ก๏ธ JSON Parser class
class TypeSafeJsonParser {
  private errors: string[] = [];
  
  parse<T>(json: string, schema: SchemaNode): T | null {
    this.errors = [];
    
    try {
      const data = JSON.parse(json);
      const validated = this.validate(data, schema, "root");
      
      if (this.errors.length > 0) {
        console.error("โŒ Validation errors:");
        this.errors.forEach(err => console.error(`  - ${err}`));
        return null;
      }
      
      return validated as T;
    } catch (error) {
      console.error("โŒ JSON parse error:", error);
      return null;
    }
  }
  
  private validate(value: unknown, schema: SchemaNode, path: string): unknown {
    // Handle optional values
    if (value === undefined || value === null) {
      if (schema.optional) return null;
      if (value === null && schema.type === "null") return null;
      this.errors.push(`${path}: required value is missing`);
      return null;
    }
    
    // Validate based on type
    switch (schema.type) {
      case "string":
      case "number":
      case "boolean":
      case "null":
        if (!primitiveGuards[schema.type](value)) {
          this.errors.push(`${path}: expected ${schema.type}, got ${typeof value}`);
          return null;
        }
        break;
        
      case "date":
        if (!primitiveGuards.date(value)) {
          this.errors.push(`${path}: invalid date value`);
          return null;
        }
        // Transform string to Date
        value = new Date(value as string);
        break;
        
      case "array":
        if (!Array.isArray(value)) {
          this.errors.push(`${path}: expected array, got ${typeof value}`);
          return null;
        }
        if (schema.items) {
          value = value.map((item, index) => 
            this.validate(item, schema.items!, `${path}[${index}]`)
          );
        }
        break;
        
      case "object":
        if (typeof value !== "object" || value === null || Array.isArray(value)) {
          this.errors.push(`${path}: expected object, got ${typeof value}`);
          return null;
        }
        if (schema.properties) {
          const result: Record<string, unknown> = {};
          
          // Validate defined properties
          for (const [key, propSchema] of Object.entries(schema.properties)) {
            result[key] = this.validate(
              (value as any)[key],
              propSchema,
              `${path}.${key}`
            );
          }
          
          // Check for extra properties
          for (const key of Object.keys(value as object)) {
            if (!(key in schema.properties)) {
              this.errors.push(`${path}: unexpected property "${key}"`);
            }
          }
          
          value = result;
        }
        break;
    }
    
    // Custom validation
    if (schema.validate && !schema.validate(value)) {
      this.errors.push(`${path}: custom validation failed`);
      return null;
    }
    
    // Custom transformation
    if (schema.transform) {
      value = schema.transform(value);
    }
    
    return value;
  }
  
  // ๐ŸŽฏ Schema builder helpers
  static schema = {
    string: (optional = false): SchemaNode => ({ type: "string", optional }),
    number: (optional = false): SchemaNode => ({ type: "number", optional }),
    boolean: (optional = false): SchemaNode => ({ type: "boolean", optional }),
    date: (optional = false): SchemaNode => ({ type: "date", optional }),
    null: (): SchemaNode => ({ type: "null" }),
    
    array: <T>(items: SchemaNode, optional = false): SchemaNode => ({
      type: "array",
      items,
      optional
    }),
    
    object: (properties: Record<string, SchemaNode>, optional = false): SchemaNode => ({
      type: "object",
      properties,
      optional
    }),
    
    // Advanced helpers
    email: (optional = false): SchemaNode => ({
      type: "string",
      optional,
      validate: (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)
    }),
    
    enum: <T extends string>(values: readonly T[], optional = false): SchemaNode => ({
      type: "string",
      optional,
      validate: (v: string) => values.includes(v as T)
    }),
    
    range: (min: number, max: number, optional = false): SchemaNode => ({
      type: "number",
      optional,
      validate: (v: number) => v >= min && v <= max
    })
  };
}

// ๐ŸŽฎ Test the parser
const parser = new TypeSafeJsonParser();
const { schema } = TypeSafeJsonParser;

// Define a user schema
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  role: "admin" | "user" | "guest";
  preferences: {
    theme: "light" | "dark";
    notifications: boolean;
  };
  tags: string[];
  createdAt: Date;
  deletedAt: Date | null;
}

const userSchema: SchemaNode = schema.object({
  id: schema.number(),
  name: schema.string(),
  email: schema.email(),
  age: schema.range(0, 150),
  role: schema.enum(["admin", "user", "guest"] as const),
  preferences: schema.object({
    theme: schema.enum(["light", "dark"] as const),
    notifications: schema.boolean()
  }),
  tags: schema.array(schema.string()),
  createdAt: schema.date(),
  deletedAt: schema.date(true) // Optional
});

// Test valid JSON
const validJson = `{
  "id": 123,
  "name": "Alice Smith",
  "email": "[email protected]",
  "age": 28,
  "role": "admin",
  "preferences": {
    "theme": "dark",
    "notifications": true
  },
  "tags": ["typescript", "developer"],
  "createdAt": "2024-01-15T10:30:00Z",
  "deletedAt": null
}`;

const user = parser.parse<User>(validJson, userSchema);
if (user) {
  console.log("โœ… Parsed user:", user);
  console.log(`๐Ÿ‘ค ${user.name} (${user.role})`);
  console.log(`๐Ÿ“… Created: ${user.createdAt.toLocaleDateString()}`);
}

// Test invalid JSON
const invalidJson = `{
  "id": "not-a-number",
  "name": "Bob",
  "email": "invalid-email",
  "age": 200,
  "role": "superadmin",
  "preferences": {
    "theme": "blue"
  },
  "tags": ["developer", 123],
  "createdAt": "invalid-date"
}`;

const invalidUser = parser.parse<User>(invalidJson, userSchema);
// Will log validation errors

// ๐ŸŽฏ Create a config schema
const configSchema = schema.object({
  api: schema.object({
    endpoint: schema.string(),
    timeout: schema.range(1000, 60000),
    retries: schema.range(0, 5, true)
  }),
  features: schema.array(schema.enum(["auth", "analytics", "notifications"] as const)),
  debug: schema.boolean(true)
});

const configJson = `{
  "api": {
    "endpoint": "https://api.example.com",
    "timeout": 5000
  },
  "features": ["auth", "analytics"],
  "debug": false
}`;

const config = parser.parse(configJson, configSchema);
if (config) {
  console.log("\n๐Ÿ”ง Parsed config:", config);
}

๐ŸŽ“ Key Takeaways

Youโ€™ve mastered type assertions and type guards! Hereโ€™s what you can now do:

  • โœ… Use type assertions responsibly when you know better ๐Ÿ’ช
  • โœ… Create custom type guards for runtime validation ๐Ÿ›ก๏ธ
  • โœ… Narrow union types with built-in guards ๐ŸŽฏ
  • โœ… Handle unknown data safely and confidently ๐Ÿ›
  • โœ… Build bulletproof APIs with proper validation! ๐Ÿš€

Remember: Type assertions say โ€œtrust me,โ€ type guards say โ€œlet me check.โ€ Always prefer checking! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered type assertions and type guards!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Complete the JSON parser exercise
  2. ๐Ÿ—๏ธ Add type guards to your existing projects
  3. ๐Ÿ“š Move on to our next tutorial: The any, unknown, never, and void Types
  4. ๐ŸŒŸ Create a validation library with your new skills!

Remember: Safe code is happy code. Type guards are your friends - use them liberally! ๐Ÿš€

Happy coding! ๐ŸŽ‰๐Ÿš€โœจ