TypeScript Interfaces and Type Aliases: Defining Custom Types

July 25, 2025

☕️ Support Us
Your support will help us to continue to provide quality content.👉 Buy Me a Coffee

In our previous articles, we've explored TypeScript's basic types and how they catch errors at compile time. Now we're ready to tackle one of TypeScript's most powerful features: creating your own custom types to describe the shape of complex objects.

Consider a function designed to display article previews that needs to pass article data between several components. The initial implementation works perfectly with complete test data.

A few weeks later, during a new feature addition, an incomplete article object gets passed to the display function. The code crashes in production when trying to access a property that doesn't exist.

// This worked with complete test data...
const createPreview = (article) => {
  return `${article.title} - ${article.content.substring(0, 100)}...`;
  // TypeError: Cannot read property 'substring' of undefined
};

// An incomplete object gets passed somewhere in the code
const incompleteArticle = { title: "Test Article" }; // Missing content!
createPreview(incompleteArticle);

If this scenario sounds familiar, you've experienced the fundamental problem that TypeScript interfaces solve. Objects in JavaScript can have any shape at any time, and there's no way to know if the shape you expect matches the shape you actually get until you try to use it.

Interfaces Are Shape Contracts

You know how frustrating it is when your code crashes because you tried to access a property that doesn't exist? That's exactly what happened in our opening example when article.content was undefined. TypeScript interfaces solve this by letting you describe what properties an object should have. Once you define an interface, TypeScript checks every object against that description and warns you if something's missing.

Think of it like a checklist. When you define an interface, you're creating a checklist that says "any object using this interface must have these specific properties." TypeScript then checks every object against that checklist.

interface Article {
  id: number;
  title: string;
  content: string;
  publishedAt: string;
}

const createPreview = (article: Article) => {
  // TypeScript guarantees article has title, content, etc.
  return `${article.title} - ${article.content.substring(0, 100)}...`;
};

Now when an incomplete object is passed, there's no runtime crash. Instead, TypeScript shows a red squiggly line in the editor with a clear error message: "Property 'content' is missing in type '{ title: string; }' but required in type 'Article'." The bug gets caught during development, not in production.

This same protection works everywhere you use the Article interface. Whether you're creating articles, displaying them, or passing them between functions, TypeScript ensures they match the expected shape.

Structure Matters, Names Don't

What can be initially confusing about TypeScript: it doesn't care what you call your objects. It only cares about their structure.

This is called structural typing, and it's different from languages like Java or C# that use nominal typing. In nominal typing, an object must be explicitly declared as a specific type to be compatible. In structural typing, TypeScript just asks: "Does this object have the right shape?" If yes, it's compatible, regardless of what you named it or how you created it.

interface Article {
  title: string;
  content: string;
  publishedAt: string;
}

// This object was never explicitly declared as an Article
const blogPost = {
  title: "Understanding TypeScript",
  content: "TypeScript helps catch bugs...",
  publishedAt: "2025-01-15",
  category: "Programming", // Extra property is fine
  views: 1250,
};

// But it matches the Article interface structure
const article: Article = blogPost; // ✅ Works perfectly

This behavior can be initially surprising. The blogPost object has extra properties (category and views) that aren't in the Article interface, but TypeScript accepts it anyway. That's because structural typing asks: "Does this object have at least the required properties?" If yes, it's compatible.

This flexibility is actually powerful. You can use the same object with multiple interfaces, each focusing on the properties it cares about.

interface Publishable {
  publishedAt: string;
}

interface Viewable {
  views: number;
}

// The same blogPost object works with all three interfaces
const canPublish: Publishable = blogPost; // ✅ Has publishedAt
const canTrack: Viewable = blogPost; // ✅ Has views
const canRead: Article = blogPost; // ✅ Has title, content, publishedAt

How This Works in Practice

Interfaces solve real problems you face when building applications.

API Response Handling: instead of guessing what shape your API returns, you define the exact structure you expect:

interface ArticleResponse {
  data: {
    articles: Article[];
    totalCount: number;
  };
  status: "success" | "error";
  message?: string; // Optional property for error cases
}

const fetchArticles = async (): Promise<ArticleResponse> => {
  const response = await fetch("/api/articles");
  const data = await response.json();

  // TypeScript now knows exactly what data contains
  // If the API changes, you'll get compile-time errors
  return data;
};

State Management: in React applications, interfaces help ensure your state updates maintain the correct shape:

interface AppState {
  articles: Article[];
  selectedArticle: Article | null;
  isLoading: boolean;
  error: string | null;
}

const [state, setState] = useState<AppState>({
  articles: [],
  selectedArticle: null,
  isLoading: false,
  error: null,
});

// TypeScript prevents you from setting invalid state
setState({ articles: "not an array" });
// ❌ Error: Type 'string' is not assignable to type 'Article[]'

Interface vs Type: Which One Should You Choose?

When talking about interface, it's inevitable that developers ask the practical question: "Should I be using type instead?" It's a reasonable concern because both syntaxes can define the exact same object shapes, and your editor treats them identically. For example, both can define object shapes:

// Using interface
interface User {
  name: string;
  email: string;
}

// Using type alias
type User = {
  name: string;
  email: string;
};

type is your default choice

But you still need to pick one, and the choice feels arbitrary until you understand what each one is really designed for. So let's understand when the differences actually matter.

The key insight is that type can do almost everything interface can do, plus some things interface cannot. This is why many developers default to type as it's the more flexible choice.

Here's what type can do that interface cannot:

// Union types - combining multiple possibilities
type Status = "draft" | "published" | "archived";

// Function types
type EventHandler = (event: Event) => void;

// Computed types based on other types
type ArticleKeys = keyof Article; // Gets 'title' | 'content' | 'publishedAt'

Try to write these with interface and you'll get errors. You can't express "one of these values" or "a function with this signature" using interface syntax.

So when would you choose interface?

First of all, when you're building class-like hierarchies with inheritance. Sometimes you have multiple types that share common properties. For example, articles, comments, and users might all have an id and createdAt timestamp. Instead of repeating these properties in each interface, you can create a base interface and extend it.

You can achieve the same thing with type using intersection types (&), but the interface extends syntax is more readable when you're building these hierarchies. In addition, extends in interface makes TypeScript's type checker run slightly faster than using & in types.

interface BaseEntity {
  id: string;
  createdAt: string;
}

// Article automatically has id and createdAt
interface Article extends BaseEntity {
  title: string;
  content: string;
}

// Comment also automatically has id and createdAt
interface Comment extends BaseEntity {
  text: string;
  authorId: string;
}

Secondly, when you need declaration merging. Declaration merging is a TypeScript feature where multiple interfaces with the same name automatically get combined into one. This sounds weird at first, but it solves a real problem.

When is this useful? Mainly when working with third-party libraries. For example, if you're using a library that defines a Window interface, but you need to add your own properties to the global window object:

// The library defines:
interface Window {
  location: Location;
  document: Document;
}

// You can extend it by declaring the same interface:
interface Window {
  myCustomProperty: string;
}

// Now Window will be like this in TypeScript's eyes
interface Window {
  location: Location;
  document: Document;
  myCustomProperty: string;
}

The downside is that declaration merging can happen accidentally. If you meant to create two different types but used the same name, TypeScript will silently merge them instead of giving you an error. With type, you get a clear error if you accidentally declare the same name twice, which prevents confusion.

Make Your Choice Consistent

Having learned the key differences, your most important decision is establishing consistency. Mixed usage of interface and type for similar purposes creates unnecessary friction for everyone working with your code.

When developers encounter inconsistent patterns, they naturally wonder: "Why interface here but type there? Is this intentional or accidental?" This questioning happens constantly across a codebase with mixed approaches, stealing focus from actual feature development.

Thus, set a rule, and then stick with it consistently. Either choice works fine. The mental clarity from consistency is what truly matters.

Summary

TypeScript interfaces solve a fundamental problem in JavaScript development: objects can have any shape, and you don't know if your assumptions are correct until runtime. By defining interfaces, you create compile-time contracts that catch shape mismatches before they reach your users.

Two fundamental concepts in interfaces are:

  • Shape contracts: Interfaces guarantee that objects have the properties you expect, catching bugs at compile-time instead of runtime.

  • Structural typing: TypeScript uses structural typing, so any object with the right properties matches your interface, regardless of how it was created.

As for the interface vs type choice, default to type for most object shapes since it's more flexible. Use interface only when you need object inheritance with extends or intentional declaration merging.

Now that you understand how to define custom types for objects, you're ready to explore TypeScript classes and objects to see how TypeScript enhances JavaScript's class-based object-oriented programming with static typing and access modifiers.

☕️ Support Us
Your support will help us to continue to provide quality content.👉 Buy Me a Coffee