TypeScript Interfaces and Types Practice: Building Real-World Type Safety
August 1, 2025
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:
- Decide whether to use
interface
ortype
for each part of the state - Create a flexible content type system that can handle different content types
- Implement proper union types for user roles and permissions
- Add type safety to the state management functions
- Explain your reasoning for choosing
interface
vstype
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, useinterface
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.