TypeScript Interfaces and Types Practice: Building Real-World Type Safety

August 1, 2025

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

Having explored TypeScript interfaces and type aliases, you now understand how custom types catch bugs at compile-time and create more maintainable code. But reading about interfaces is quite different from using them to solve real problems in your own projects.

These practice exercises will help you internalize the concepts through hands-on coding. Each question builds on the previous one, starting with basic interface usage and progressing to complex scenarios you'll encounter in production applications.

Question 1 (Easy): Creating Your First Interface Contract

You're building a blog application and need to ensure that all article objects have the required properties. Currently, the code is written in JavaScript and occasionally crashes when incomplete article data is passed around.

Convert this JavaScript code to TypeScript by creating an appropriate interface:

const displayArticle = (article) => {
  const publishDate = new Date(article.publishedAt).toLocaleDateString();
  return `
    <div class="article">
      <h2>${article.title}</h2>
      <p class="meta">Published: ${publishDate} | Author: ${article.author}</p>
      <div class="content">${article.content}</div>
    </div>
  `;
};

const myArticle = {
  title: "Getting Started with TypeScript",
  author: "ExplainThis",
  content: "TypeScript is a powerful superset of JavaScript...",
  publishedAt: "2025-01-15",
};

console.log(displayArticle(myArticle));

Your task: Define an interface for the article object and add proper type annotations to make this code type-safe.

Solution and Explanation

Let's start by looking at what properties the displayArticle function actually uses. Notice what happens here: the function accesses article.title, article.publishedAt, article.author, and article.content. This tells you exactly what properties your interface needs to guarantee.

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

const displayArticle = (article: Article): string => {
  const publishDate = new Date(article.publishedAt).toLocaleDateString();
  return `
    <div class="article">
      <h2>${article.title}</h2>
      <p class="meta">Published: ${publishDate} | Author: ${article.author}</p>
      <div class="content">${article.content}</div>
    </div>
  `;
};

const myArticle: Article = {
  title: "Getting Started with TypeScript",
  author: "ExplainThis",
  content: "TypeScript is a powerful superset of JavaScript...",
  publishedAt: "2025-01-15",
};

console.log(displayArticle(myArticle));

Why is this better than the original JavaScript version? The interface creates a contract. If someone tries to pass an incomplete article object, TypeScript catches it at compile-time:

const incompleteArticle = {
  title: "My Article",
  author: "John Doe",
  // Missing content and publishedAt!
};

displayArticle(incompleteArticle);
// ❌ TypeScript Error: Argument of type '{ title: string; author: string; }'
// is not assignable to parameter of type 'Article'.
// Property 'content' is missing in type '{ title: string; author: string; }'

This is the fundamental value of interfaces: they guarantee that objects have the shape you expect, preventing runtime crashes from missing properties.

Question 2 (Easy-Medium): Handling Optional Properties and API Responses

Sometimes APIs return incomplete data. You're building a blog preview system where some articles have content loaded, others don't:

// Some articles have content, others are just metadata
const articles = [
  {
    id: 1,
    title: "Complete Article",
    author: "Jane Doe",
    content: "This article has full content...",
    tags: ["typescript", "javascript"],
  },
  {
    id: 2,
    title: "Preview Only",
    author: "John Smith",
    // Notice: no content or tags!
  },
];

const createPreview = (article) => {
  const preview = article.content
    ? article.content.substring(0, 50) + "..."
    : "No preview available";

  return {
    id: article.id,
    title: article.title,
    preview,
  };
};

Your task: Create an interface that handles articles with optional content and tags properties, and add proper typing to the createPreview function.

Solution and Explanation

Let's think about this step by step. We have two different shapes of article objects: some have complete data, others are missing content and tags. How do we handle this with TypeScript?

The key insight is using optional properties. When you add a ? after a property name, you're telling TypeScript "this property might not exist, so check before using it."

interface Article {
  id: number;
  title: string;
  author: string;
  content?: string; // Optional: might not be loaded yet
  tags?: string[]; // Optional: might not be assigned yet
}

interface ArticlePreview {
  id: number;
  title: string;
  preview: string;
}

const createPreview = (article: Article): ArticlePreview => {
  const preview = article.content
    ? article.content.substring(0, 50) + "..."
    : "No preview available";

  return {
    id: article.id,
    title: article.title,
    preview,
  };
};

// Now this works safely with both complete and partial articles
const articles: Article[] = [
  {
    id: 1,
    title: "Complete Article",
    author: "Jane Doe",
    content: "This article has full content...",
    tags: ["typescript", "javascript"],
  },
  {
    id: 2,
    title: "Preview Only",
    author: "John Smith",
    // Missing content and tags - that's okay because they're optional!
  },
];

articles.forEach(article => {
  console.log(createPreview(article));
});

Notice what happens when we use optional properties: TypeScript knows that article.content might be undefined, so it requires you to check before using methods like substring(). This is exactly the safety net you want, the code won't crash if the property is missing.

Why did we create a separate ArticlePreview interface? Because the preview has a different shape than the original article. The preview always has a preview string, even when the original article's content property was undefined. This demonstrates how interfaces help you think clearly about data transformations in your application.

Question 3 (Medium): Type Aliases vs Interfaces in Complex State Management

You're building a React application with complex state management for your blog platform. The application needs to handle different types of content (articles, videos, podcasts), user permissions, and loading states. You need to decide when to use interface vs type and create a flexible type system that can grow with your application.

Here's the current state structure that needs proper typing:

// Current untyped state - this needs proper TypeScript interfaces/types
const initialState = {
  // Content can be different types
  content: {
    articles: [
      {
        id: "1",
        type: "article",
        title: "My Article",
        content: "...",
        author: "Jane",
        publishedAt: "2025-01-15",
      },
    ],
    videos: [
      {
        id: "2",
        type: "video",
        title: "My Video",
        url: "https://...",
        duration: 300,
        author: "John",
        publishedAt: "2025-01-14",
      },
    ],
    podcasts: [
      {
        id: "3",
        type: "podcast",
        title: "My Podcast",
        audioUrl: "https://...",
        duration: 1800,
        author: "Alice",
        publishedAt: "2025-01-13",
      },
    ],
  },

  // User permissions vary by role
  user: {
    id: "user123",
    name: "Current User",
    role: "editor", // "admin" | "editor" | "viewer"
    permissions: ["read", "write", "publish"], // depends on role
  },

  // UI state for different operations
  ui: {
    selectedContent: null, // Could be any content type
    isLoading: false,
    error: null,
    filters: {
      contentType: "all", // "all" | "article" | "video" | "podcast"
      author: null,
      dateRange: { start: null, end: null },
    },
  },
};

// Functions that work with this state
const selectContent = (state, contentId, contentType) => {
  const contentArray = state.content[contentType + "s"]; // articles, videos, podcasts
  const selectedItem = contentArray.find((item) => item.id === contentId);
  return { ...state, ui: { ...state.ui, selectedContent: selectedItem } };
};

const canUserEdit = (user, content) => {
  if (user.role === "admin") return true;
  if (user.role === "editor" && content.author === user.name) return true;
  return false;
};

Your tasks:

  1. Decide whether to use interface or type for each part of the state
  2. Create a flexible content type system that can handle different content types
  3. Implement proper union types for user roles and permissions
  4. Add type safety to the state management functions
  5. Explain your reasoning for choosing interface vs type in each case

Solution and Explanation

This is a complex typing challenge that mirrors real-world applications. Let's think through each decision carefully, considering when interface vs type makes the most sense.

First, let's analyze what we're dealing with. We have shared properties across different content types (all have id, title, author, publishedAt), user roles with different permission sets, and complex state with nested objects. This suggests we'll need both inheritance patterns and union types.

// Use interface for shared structure that other types will extend
interface BaseContent {
  id: string;
  title: string;
  author: string;
  publishedAt: string;
}

// Use interface for each content type since they extend BaseContent
interface Article extends BaseContent {
  type: "article";
  content: string;
}

interface Video extends BaseContent {
  type: "video";
  url: string;
  duration: number;
}

interface Podcast extends BaseContent {
  type: "podcast";
  audioUrl: string;
  duration: number;
}

// Use type for union types - interface can't express "one of these"
type ContentItem = Article | Video | Podcast;
type ContentType = "article" | "video" | "podcast";
type UserRole = "admin" | "editor" | "viewer";
type Permission = "read" | "write" | "publish" | "delete";

// Use interface for object shapes that might be extended later
interface User {
  id: string;
  name: string;
  role: UserRole;
  permissions: Permission[];
}

interface DateRange {
  start: string | null;
  end: string | null;
}

interface Filters {
  contentType: "all" | ContentType;
  author: string | null;
  dateRange: DateRange;
}

interface UIState {
  selectedContent: ContentItem | null;
  isLoading: boolean;
  error: string | null;
  filters: Filters;
}

interface ContentState {
  articles: Article[];
  videos: Video[];
  podcasts: Podcast[];
}

interface AppState {
  content: ContentState;
  user: User;
  ui: UIState;
}

// Now our functions can be properly typed
const selectContent = (
  state: AppState,
  contentId: string,
  contentType: ContentType
): AppState => {
  const contentArray = state.content[`${contentType}s` as keyof ContentState];
  const selectedItem = contentArray.find((item) => item.id === contentId);

  return {
    ...state,
    ui: {
      ...state.ui,
      selectedContent: selectedItem || null,
    },
  };
};

const canUserEdit = (user: User, content: ContentItem): boolean => {
  if (user.role === "admin") return true;
  if (user.role === "editor" && content.author === user.name) return true;
  return false;
};

// Type-safe state initialization
const initialState: AppState = {
  content: {
    articles: [
      {
        id: "1",
        type: "article",
        title: "My Article",
        content: "...",
        author: "Jane",
        publishedAt: "2025-01-15",
      },
    ],
    videos: [
      {
        id: "2",
        type: "video",
        title: "My Video",
        url: "https://...",
        duration: 300,
        author: "John",
        publishedAt: "2025-01-14",
      },
    ],
    podcasts: [
      {
        id: "3",
        type: "podcast",
        title: "My Podcast",
        audioUrl: "https://...",
        duration: 1800,
        author: "Alice",
        publishedAt: "2025-01-13",
      },
    ],
  },
  user: {
    id: "user123",
    name: "Current User",
    role: "editor",
    permissions: ["read", "write", "publish"],
  },
  ui: {
    selectedContent: null,
    isLoading: false,
    error: null,
    filters: {
      contentType: "all",
      author: null,
      dateRange: { start: null, end: null },
    },
  },
};

Let's break down the interface vs type decisions:

Why interface for content types? We used interface for BaseContent, Article, Video, and Podcast because we have a clear inheritance hierarchy. The extends keyword makes the relationship explicit and performs better than intersection types (&) for the TypeScript compiler.

Why type for unions? Union types like ContentItem = Article | Video | Podcast can only be expressed with type. Interface simply cannot represent "one of these options."

Why interface for state objects? We used interface for AppState, UIState, etc., because these are object shapes that might need to be extended or modified as the application grows. Interfaces also support declaration merging if you need to augment them from different modules.

Notice how this typing system catches errors before they happen. If you try to access content.audioUrl on an Article, TypeScript prevents it. If you try to assign an invalid role like "super-admin", you get a compile-time error. The type system guides you toward correct usage patterns.

This approach scales well because adding a new content type like Newsletter only requires defining the interface and adding it to the ContentItem union. The rest of your code automatically gets type safety for the new content type.

Wrapping Up

These exercises demonstrate how TypeScript interfaces and type aliases solve real-world development problems. From basic object validation to complex state management, type safety prevents bugs and makes your code more maintainable.

The key insights to remember:

  • Interfaces create contracts that catch missing properties at compile-time, not runtime
  • Optional properties handle incomplete data safely without crashes
  • Union types express "one of these options" scenarios that interfaces cannot
  • Structural typing means objects match interfaces based on shape, not explicit declaration
  • Choose type by default for flexibility, use interface for inheritance hierarchies

With these foundations solid, you're ready to explore TypeScript classes and objects to see how static typing enhances object-oriented programming patterns.

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