Isaac.

Type Safety in TypeScript

Leverage TypeScript's type system for safer, more maintainable code.

By EMEPublished: February 20, 2025
typescripttypestype safetygenericsinterfaces

A Simple Analogy

Types in TypeScript are like contract agreements. They specify what's promised and catch violations at compile-time.


Why Type Safety?

  • Fewer bugs: Errors at compile-time
  • Better IDE support: Autocomplete and suggestions
  • Documentation: Types document intent
  • Refactoring: Safe code changes
  • Maintainability: Easier to understand

Basic Types

// Primitives
const name: string = "Alice";
const age: number = 30;
const active: boolean = true;
const nothing: null = null;
const unknown: undefined = undefined;

// Collections
const numbers: number[] = [1, 2, 3];
const mixed: (string | number)[] = [1, "two"];
const tuple: [string, number] = ["age", 30];

// Objects
interface User {
  id: number;
  name: string;
  email?: string;  // Optional
  readonly createdAt: Date;  // Readonly
}

const user: User = {
  id: 1,
  name: "Bob",
  createdAt: new Date()
};

Generics

// Generic function
function getFirst<T>(items: T[]): T {
  return items[0];
}

const firstNumber = getFirst([1, 2, 3]);  // number
const firstString = getFirst(["a", "b"]);  // string

// Generic interface
interface Container<T> {
  value: T;
  getValue(): T;
}

const numberContainer: Container<number> = {
  value: 42,
  getValue() { return this.value; }
};

// Generic constraints
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const merged = merge({ a: 1 }, { b: 2 });  // { a: 1; b: 2 }

Union and Intersection Types

// Union: one of multiple types
type Status = "pending" | "completed" | "failed";
type ID = string | number;

function processStatus(status: Status) {
  switch (status) {
    case "pending": return "Waiting...";
    case "completed": return "Done!";
    case "failed": return "Error";
  }
}

// Intersection: combination of types
interface Timestamped {
  createdAt: Date;
  updatedAt: Date;
}

interface Named {
  name: string;
}

type Document = Timestamped & Named;

const doc: Document = {
  name: "Report",
  createdAt: new Date(),
  updatedAt: new Date()
};

Utility Types

// Partial: all properties optional
type PartialUser = Partial<User>;

// Required: all properties required
type RequiredUser = Required<User>;

// Readonly: all properties readonly
type ReadonlyUser = Readonly<User>;

// Pick: select specific properties
type UserPreview = Pick<User, "id" | "name">;

// Omit: exclude specific properties
type UserWithoutEmail = Omit<User, "email">;

// Record: object with specific keys
type Role = "admin" | "user" | "guest";
const permissions: Record<Role, boolean> = {
  admin: true,
  user: true,
  guest: false
};

// Exclude: remove types from union
type NonString = Exclude<string | number | boolean, string>;  // number | boolean

Type Guards

// Type predicate
function isUser(obj: unknown): obj is User {
  return (
    typeof obj === "object" &&
    obj !== null &&
    "id" in obj &&
    "name" in obj &&
    typeof obj.id === "number" &&
    typeof obj.name === "string"
  );
}

// Usage
const data: unknown = JSON.parse(jsonString);
if (isUser(data)) {
  console.log(data.name);  // Safe: TypeScript knows it's User
}

// instanceof guard
if (error instanceof RangeError) {
  console.log("Range error occurred");
}

// Discriminated unions
type Result = 
  | { status: "success"; data: string }
  | { status: "error"; error: Error };

function handleResult(result: Result) {
  if (result.status === "success") {
    console.log(result.data);
  } else {
    console.log(result.error);
  }
}

Best Practices

  1. Avoid any: Use specific types
  2. Enable strict mode: Catch more errors
  3. Use type guards: Safe type narrowing
  4. Document with types: Types are documentation
  5. Leverage inference: Let TypeScript infer when possible

Related Concepts

  • Advanced generics
  • Conditional types
  • Mapped types
  • Type inference

Summary

TypeScript's type system prevents entire categories of bugs at compile-time. Use interfaces, generics, and utility types to build robust applications.