Right, let’s talk about interface. This is where you stop just typing keys and values and start declaring your intentions. Think of an interface as a contract. It’s your way of telling the TypeScript compiler, “Look, any object that says it implements this User interface must have these specific properties, with these specific types. No ifs, ands, or buts.” It’s the single best tool for bringing order to the chaos of JavaScript object shapes.

The syntax is beautifully straightforward. You use the interface keyword, give it a name (PascalCase by convention, because we’re not animals), and then describe the shape inside the braces.

interface User {
  id: number;
  username: string;
  email: string;
  isActive: boolean;
}

Now, any object you annotate with : User must conform to this exact structure. Try to pass it something with a missing property or a wrong type, and TypeScript will throw a fit at you—which is exactly what you want. It’s catching your mistakes before you run the code.

const myUser: User = {
  id: 1,
  username: "coolDev42",
  email: "dev@example.com",
  isActive: true
}; // All good!

const badUser: User = {
  id: 2,
  username: "oopsNoEmail"
}; // Property 'email' is missing. TypeScript has already saved you.

Optional Properties: The Escape Hatch

Sometimes, not every property is required. Maybe a user has an optional avatarUrl or a dateOfBirth. For this, we use the question mark (?). This tells TypeScript, “This property can exist, but it doesn’t have to.” It’s the difference between a strict bouncer with a guest list and one who’s cool if your friend shows up later.

interface User {
  id: number;
  username: string;
  email: string;
  isActive: boolean;
  avatarUrl?: string; // This one is optional
}

const userWithAvatar: User = {
  id: 3,
  username: "hasPic",
  email: "pic@example.com",
  isActive: true,
  avatarUrl: "https://example.com/pic.jpg"
};

const userWithoutAvatar: User = {
  id: 4,
  username: "noPic",
  email: "nopic@example.com",
  isActive: true
}; // Also perfectly valid. No avatarUrl? No problem.

Readonly Properties: Drawing a Line in the Sand

There are things that, once set, should never change. A user’s id is a classic example. You can enforce this at the type level with the readonly modifier. It’s like putting a “Do Not Touch” sign on a property. You can assign its value when the object is first created, but any subsequent attempt to change it is a compile-time error.

interface User {
  readonly id: number; // This is set once and never changed
  username: string;
  // ... other properties
}

const user: User = {
  id: 42,
  username: "theAnswer"
};

user.username = "newUsername"; // Fine, usernames can change.
user.id = 24; // ERROR: Cannot assign to 'id' because it is a read-only property.

This is a fantastic way to prevent accidental mutations and document your intent. It doesn’t provide any runtime immutability (a determined developer can still override it with as any), but it’s a powerful first line of defense.

Methods and Function Types

Interfaces aren’t just for data; they can also describe functions. You can define the signature of a method that an object must implement. There are two common ways to do this, and I’ll be honest, the first one looks a bit weird but is more common.

interface Database {
  get(id: number): User; // Method signature syntax
  delete: (id: number) => boolean; // Function type syntax
}

const myDB: Database = {
  get(id) {
    // fetch user logic
    return { id, username: "test", email: "a@b.com", isActive: true };
  },
  delete(id) {
    // delete logic
    return true;
  }
};

Both are correct. The method syntax (get(id: number): User) is often preferred for describing methods on an object, while the function type syntax (delete: (id: number) => boolean) is more aligned with how we describe standalone functions. Know both, use the one that fits the context best.

The extends Keyword: Interface Inheritance

This is one of the killer features of interface. You can create a new interface that inherits all the properties and methods of an existing one. This is perfect for creating more specific versions of a general type without repeating yourself (DRY—Don’t Repeat Yourself—is a good thing).

interface BaseUser {
  id: number;
  username: string;
}

interface AdminUser extends BaseUser {
  role: 'admin' | 'superadmin'; // New property specific to AdminUser
  permissions: string[];
}

const admin: AdminUser = {
  id: 1,
  username: "boss",
  role: "admin", // Required from AdminUser
  permissions: ["delete", "ban"] // Required from AdminUser
};
// Also must have `id` and `username` from BaseUser!

You can even extend multiple interfaces (interface A extends B, C { ... }), which is incredibly useful for composing complex types from simpler, reusable ones. It’s like building with LEGO instead of carving everything from a single block of plastic.