"Should I use type or interface?" This question has sparked mass debates across over 2,000 GitHub issues, Reddit threads, and Stack Overflow questions. The TypeScript team's official stance? "For the most part, you can choose based on personal preference." Yet the nuances between them reveal deep understanding of TypeScript's design philosophy—which is exactly why interviewers keep asking.
Here's how to answer with confidence and demonstrate genuine understanding.
The 30-Second Answer
When you're pressed for time in an interview, here's what you need to communicate: both type and interface can describe object shapes, but they have different strengths. Interfaces are best for defining contracts for objects and classes, support declaration merging, and can be extended. Types are more flexible, supporting unions, tuples, primitives, and advanced type operations like conditional types. For simple object shapes, either works, but consistency across your codebase matters more than the choice itself.
If you only remember one thing, remember this: use interface for public APIs and object contracts, use type when you need unions, mapped types, or other advanced features that interfaces don't support.
The 2-Minute Answer (When They Want More Depth)
When an interviewer gives you room to elaborate, this is your chance to demonstrate real understanding. Here's how I'd explain it in a technical discussion:
TypeScript gives us two primary ways to define types: type aliases (using the type keyword) and interfaces. While they overlap significantly when defining object shapes, they each have unique capabilities that make them suited for different scenarios.
Interfaces are specifically designed for defining the shape of objects and class contracts. They support declaration merging, meaning you can define the same interface multiple times and TypeScript will combine them. This is incredibly powerful when you're working with third-party libraries or need to augment global types. Interfaces also have slightly better performance during type checking because of their simpler internal representation.
Type aliases, on the other hand, are more versatile. They can represent anything: primitives, unions, tuples, function signatures, and complex computed types. When you need conditional types, mapped types, or template literal types, you'll reach for type. Types also work better when you're building complex type utilities or working with generic constraints.
The key insight here is that they're not competing features; they're complementary tools. A pattern that's served me well is using interfaces for defining contracts that other parts of the code or external consumers will implement, while using types for internal utilities and complex type operations.
Understanding the Fundamentals
Before we dive into the differences, let's establish a solid foundation. Think of interface as a contract or specification sheet. When you define an interface, you're saying "anything that implements this contract must have these properties and methods." It's like a blueprint that classes or objects must follow.
A type alias, on the other hand, is more like giving a name to any type expression. You're not creating a new type; you're creating a convenient alias for an existing type or combination of types. This subtle distinction is what gives types their flexibility.
Let me show you with code:
// Interface: A contract for objects
interface User {
id: number;
name: string;
email: string;
}
// Type alias: A name for a type expression
type UserType = {
id: number;
name: string;
email: string;
};
// Both work identically for basic object typing
const user1: User = { id: 1, name: "Alice", email: "alice@example.com" };
const user2: UserType = { id: 2, name: "Bob", email: "bob@example.com" };At first glance, these look identical, and for simple object definitions, they behave the same way. The differences emerge when we start doing more complex things.
Where Interfaces Shine: Declaration Merging
One of the most powerful features unique to interfaces is declaration merging. When you declare an interface with the same name multiple times, TypeScript automatically merges them into a single interface containing all the properties.
Here's where it gets interesting:
// First declaration
interface Config {
apiUrl: string;
timeout: number;
}
// Second declaration - TypeScript merges these
interface Config {
debugMode: boolean;
retryCount: number;
}
// The merged interface has all four properties
const config: Config = {
apiUrl: "https://api.example.com",
timeout: 5000,
debugMode: true,
retryCount: 3
};This walkthrough shows how TypeScript combines both declarations seamlessly. The first Config interface defines apiUrl and timeout, while the second adds debugMode and retryCount. When you use the Config interface, it requires all four properties.
This might seem like an edge case, but it's invaluable when working with third-party libraries. Say you're using a library that exports a Window interface. You can extend it without modifying the library's code:
// Extending the global Window interface
declare global {
interface Window {
analytics: {
track: (event: string, properties?: object) => void;
};
}
}
// Now TypeScript knows about window.analytics
window.analytics.track("page_view", { page: "/home" });Types cannot do this. If you try to declare a type with the same name twice, TypeScript will give you an error:
type Config = { apiUrl: string };
type Config = { timeout: number }; // Error: Duplicate identifier 'Config'Where Types Excel: Unions and Advanced Type Operations
While interfaces own the declaration merging space, types dominate when it comes to expressiveness and flexibility. Let me show you what I mean.
Unions are perhaps the most common use case where you'll reach for type:
// Union types - only possible with type
type Status = "pending" | "approved" | "rejected";
type NumericId = number | bigint;
type Nullable<T> = T | null | undefined;
// You cannot create these with interfaces
interface Status = "pending" | "approved" | "rejected"; // Syntax error!The Status type above is a union of string literals, meaning a variable of type Status can only be one of those three specific strings. This is incredibly powerful for creating type-safe enumerations without the runtime overhead of TypeScript enums.
Types also excel at creating utility types and working with mapped types:
// Mapped type - creating a read-only version of any type
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Conditional type - extracting return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Template literal types
type EventName = `on${Capitalize<string>}`;
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiEndpoint = `/${string}`;
// Combining them
type ApiRoute = `${HttpMethod} ${ApiEndpoint}`;
// Valid: "GET /users", "POST /orders"This code demonstrates how types can perform computations at the type level. The Readonly<T> type takes any type T and creates a new type where every property is read-only. The ReturnType<T> uses conditional types and the infer keyword to extract the return type of a function. These operations are either impossible or much more cumbersome with interfaces.
Inheritance and Extension: Different Approaches
Both interfaces and types support inheritance, but they do it differently, and understanding this difference is crucial for interview success.
Interfaces use the extends keyword, which should feel familiar if you've worked with object-oriented programming:
interface Animal {
name: string;
age: number;
}
interface Dog extends Animal {
breed: string;
bark(): void;
}
// Multiple interface extension
interface Pet {
owner: string;
}
interface HouseDog extends Dog, Pet {
isHouseTrained: boolean;
}
const myDog: HouseDog = {
name: "Max",
age: 3,
breed: "Labrador",
bark() { console.log("Woof!"); },
owner: "Alice",
isHouseTrained: true
};This example shows how Dog extends Animal, inheriting its properties while adding its own. HouseDog then extends both Dog and Pet, demonstrating multiple inheritance which interfaces support naturally.
Types use intersection (&) for combining types:
type Animal = {
name: string;
age: number;
};
type Dog = Animal & {
breed: string;
bark(): void;
};
type Pet = {
owner: string;
};
type HouseDog = Dog & Pet & {
isHouseTrained: boolean;
};The result is functionally equivalent, but there's a subtle semantic difference. With interfaces, you're saying "Dog IS an Animal." With types, you're saying "Dog is a type that has all properties of Animal AND these additional properties." In practice, both work great, but the choice can affect how you think about your type hierarchy.
Here's where it gets interesting: interfaces and types can work together:
// Interface extending a type
type BaseUser = {
id: number;
email: string;
};
interface AdminUser extends BaseUser {
adminLevel: number;
permissions: string[];
}
// Type intersecting with an interface
interface Timestamps {
createdAt: Date;
updatedAt: Date;
}
type AuditableUser = AdminUser & Timestamps;The candidates who impress me are those who recognize that interfaces and types are interoperable, not competing features.
What Interviewers Are Really Looking For
When I ask about type vs interface, I'm not just testing syntax knowledge. I'm looking for several things:
First, I want to see practical understanding. Can you explain when you'd choose one over the other in a real project? Saying "I use interface for objects" is surface-level. Saying "I use interfaces for API contracts that might be extended by consumers, and types for internal utilities and union types" shows deeper thinking.
Second, I'm assessing your attention to team conventions. The best answer often includes "...but I'd follow whatever convention the existing codebase uses for consistency." This shows maturity and the understanding that readable, consistent code is more valuable than theoretical purity.
Third, I'm looking for awareness of the edge cases. Do you know about declaration merging? Can you explain why you'd use a type for a function signature? These details separate senior developers from intermediate ones.
A pattern that I've seen in strong candidates is that they acknowledge the overlap and focus on the practical differences:
Use interface when:
- Defining public APIs that others will implement or extend
- Creating class contracts with
implements - You need declaration merging for library augmentation
- Working with object shapes that might be extended later
Use type when:
- Creating unions of literals or other types
- Defining tuple types
- Using mapped types, conditional types, or template literal types
- Creating utility types
- The type represents something other than an object shape
Classic Interview Problems with Solutions
Let me walk you through some problems I've seen in actual interviews and how to approach them.
Problem 1: Type-Safe Event System
"Create a type-safe event emitter where events and their payloads are type-checked."
This problem tests your understanding of both interfaces and types, and how they work together:
// Define events using a type (because we want mapped types)
type EventMap = {
userCreated: { userId: string; email: string };
orderPlaced: { orderId: string; amount: number };
paymentProcessed: { transactionId: string; status: "success" | "failed" };
};
// Interface for the emitter contract
interface TypedEventEmitter<T extends Record<string, any>> {
on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void;
emit<K extends keyof T>(event: K, payload: T[K]): void;
off<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void;
}
// Implementation
class EventEmitter<T extends Record<string, any>> implements TypedEventEmitter<T> {
private handlers: Map<keyof T, Set<(payload: any) => void>> = new Map();
on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
}
emit<K extends keyof T>(event: K, payload: T[K]): void {
const eventHandlers = this.handlers.get(event);
if (eventHandlers) {
eventHandlers.forEach(handler => handler(payload));
}
}
off<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void {
const eventHandlers = this.handlers.get(event);
if (eventHandlers) {
eventHandlers.delete(handler);
}
}
}
// Usage - fully type-safe!
const emitter = new EventEmitter<EventMap>();
emitter.on("userCreated", (payload) => {
// TypeScript knows payload has userId and email
console.log(`User ${payload.userId} created with email ${payload.email}`);
});
emitter.emit("orderPlaced", { orderId: "123", amount: 99.99 }); // ✓ Type-safe
emitter.emit("orderPlaced", { orderId: "123" }); // ✗ Error: missing 'amount'This solution uses a type for EventMap because we need the flexibility of a record type that we can iterate over with keyof. We use an interface for TypedEventEmitter because it's a contract that classes implement. This combination demonstrates practical understanding of when to use each.
Problem 2: Building a Flexible API Response Type
"Create a type that represents API responses, handling success and error cases with proper type narrowing."
// Use types for union and discriminated unions
type ApiResponse<T> =
| { status: "success"; data: T; timestamp: Date }
| { status: "error"; error: { code: number; message: string }; timestamp: Date }
| { status: "loading" };
// Interface for the shape of specific response data
interface UserData {
id: string;
name: string;
email: string;
}
interface OrderData {
orderId: string;
items: Array<{ productId: string; quantity: number }>;
total: number;
}
// Utility type for extracting successful response data
type ExtractData<T> = T extends { status: "success"; data: infer D } ? D : never;
// Usage with type narrowing
function handleResponse<T>(response: ApiResponse<T>): T | null {
switch (response.status) {
case "success":
// TypeScript knows response.data exists and is type T
console.log(`Success at ${response.timestamp}`);
return response.data;
case "error":
// TypeScript knows response.error exists
console.error(`Error ${response.error.code}: ${response.error.message}`);
return null;
case "loading":
// TypeScript knows this is the loading state
console.log("Still loading...");
return null;
}
}
// Example usage
const userResponse: ApiResponse<UserData> = {
status: "success",
data: { id: "1", name: "Alice", email: "alice@example.com" },
timestamp: new Date()
};
const user = handleResponse(userResponse); // Type: UserData | nullThis problem showcases discriminated unions (which require types) combined with interfaces for specific data shapes. The interviewer is looking for your ability to use both tools effectively together.
Problem 3: Extend a Third-Party Library Type
"You're using a third-party API client that returns generic response objects. Extend it to add your app-specific headers."
// Imagine this comes from a third-party library
interface RequestConfig {
url: string;
method: "GET" | "POST" | "PUT" | "DELETE";
timeout?: number;
}
// Use declaration merging to extend it
interface RequestConfig {
headers?: {
Authorization?: string;
"X-Request-Id"?: string;
"X-App-Version"?: string;
};
retryPolicy?: {
maxRetries: number;
backoffMs: number;
};
}
// Now RequestConfig has all properties from both declarations
function makeRequest(config: RequestConfig): Promise<Response> {
console.log(`Making ${config.method} request to ${config.url}`);
if (config.headers?.Authorization) {
console.log("Request is authenticated");
}
if (config.retryPolicy) {
console.log(`Will retry up to ${config.retryPolicy.maxRetries} times`);
}
// Implementation...
return fetch(config.url, {
method: config.method,
headers: config.headers as HeadersInit
});
}This demonstrates declaration merging in action, one of the key differentiators of interfaces. In a real-world scenario, this is how you'd add type safety to libraries that don't have complete type definitions.
Common Follow-Up Questions
Interviewers often dig deeper after your initial answer. Here's how I'd approach the common follow-ups:
"Can you explain why interfaces are slightly more performant?"
Great question. TypeScript's compiler has an optimization for interfaces: they're cached by name during type checking. When the compiler encounters an interface, it can quickly look it up. Types, especially complex ones with unions or conditional logic, need to be computed and compared structurally each time. In practice, this difference is negligible for most projects, but it's why the TypeScript team recommends interfaces when you don't need type-specific features.
"When would you choose a type over an interface for a simple object?"
Honestly, for a simple object with no special requirements, I'd follow the existing codebase convention. If starting fresh, I slightly prefer interfaces for objects because of the performance benefit and because they're more "extendable" if requirements change. But if I'm already using types extensively for utility types in that file, I might use a type for consistency within that file.
"How do you handle a situation where team members disagree on type vs interface?"
This is really a process question disguised as a technical one. I'd suggest establishing a clear guideline in the project's style guide. Something like: "Use interfaces for object shapes and class contracts; use types for unions, utilities, and computed types." Then enforce it with ESLint rules like @typescript-eslint/consistent-type-definitions. The key is picking a convention and sticking to it, rather than debating each case.
"Can you implement an interface with a class? What about a type?"
Yes to both! Classes can implement both interfaces and types, as long as the type represents an object structure:
interface IUser {
id: number;
getName(): string;
}
type TUser = {
id: number;
getName(): string;
};
class InterfaceUser implements IUser {
id: number = 1;
getName() { return "Interface User"; }
}
class TypeUser implements TUser {
id: number = 2;
getName() { return "Type User"; }
}Both work identically. The difference is that if you later need to augment the type through declaration merging, only the interface version will support that.
Quick Reference: Type vs Interface at a Glance
| Feature | Interface | Type |
|---|---|---|
| Object shapes | Yes | Yes |
| Declaration merging | Yes | No |
| Extends/inheritance | extends keyword | Intersection & |
| Implements (classes) | Yes | Yes |
| Union types | No | Yes |
| Tuple types | No | Yes |
| Primitive aliases | No | Yes |
| Mapped types | Limited | Full support |
| Conditional types | No | Yes |
| Computed properties | No | Yes |
| Performance | Slightly better | Slightly slower |
| Error messages | Usually clearer | Can be complex |
Practice Questions with Answers
Test your understanding with these practice problems:
Question 1: You need to create a type that represents either a successful database query result or an error. Which would you use and why?
Answer: Use a type because this requires a discriminated union:
type QueryResult<T> =
| { success: true; data: T; rowCount: number }
| { success: false; error: string; code: number };Interfaces cannot represent unions, making type the only choice here.
Question 2: You're defining a contract for React components that need specific lifecycle methods. Interface or type?
Answer: Use an interface because you're defining a contract that classes/components will implement:
interface LifecycleComponent {
componentDidMount?(): void;
componentWillUnmount?(): void;
shouldComponentUpdate?(nextProps: unknown): boolean;
}Interfaces communicate "this is a contract to be implemented" more clearly than types.
Question 3: You need to create a utility type that makes all properties of an object optional except for specified required keys. How would you approach this?
Answer: Use a type because this requires mapped types and conditional logic:
type RequireOnly<T, K extends keyof T> = Partial<T> & Pick<T, K>;
interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
type CreateUserInput = RequireOnly<User, "name" | "email">;
// Result: { id?: number; name: string; email: string; avatar?: string }Question 4: You're extending a library's configuration interface to add company-specific options. Which approach works?
Answer: Use interface declaration merging:
// Extend the library's interface
declare module "some-library" {
interface Config {
companyId: string;
environment: "dev" | "staging" | "prod";
}
}This is the only way to extend third-party types without modifying their source code.
Wrapping Up: What This Means for Your Interview
The type vs interface question is a gateway to demonstrating your depth as a TypeScript developer. When you answer it well, you show that you understand not just the syntax, but the design philosophy behind TypeScript's type system.
Remember these key takeaways:
- Both are valid for object shapes; consistency matters more than the choice
- Interfaces excel at declaration merging and defining implementable contracts
- Types excel at unions, computed types, and advanced type operations
- They're complementary tools, not competitors
- Follow your team's conventions; advocate for clear guidelines if none exist
The candidates who impress me most are those who can explain the trade-offs thoughtfully and adapt their answer to the specific context of the question. Show that you've thought about this, used both in real projects, and can make pragmatic decisions.
Ready to Master TypeScript Interview Questions?
Understanding type vs interface is just one piece of the TypeScript interview puzzle. Our comprehensive question bank covers everything from generics and utility types to advanced patterns that senior developers are expected to know.
Access 800+ TypeScript Interview Questions and practice with real interview scenarios, detailed explanations, and expert tips that helped thousands of developers land their dream jobs.
Whether you're preparing for your first TypeScript role or leveling up to senior positions, our curated question collection gives you the edge you need. Start practicing today and walk into your next interview with confidence.
Related Articles
If you found this helpful, check out these related guides:
- Complete Frontend Developer Interview Guide - comprehensive preparation guide for frontend interviews
- TypeScript Generics Interview Guide - Master generic types, constraints, and utility types
- 7 Tricky TypeScript Interview Questions - Advanced type system questions that test deep knowledge
- React 19 Interview Guide - New features like Actions, use() hook, and Server Components
