"Should I use type or interface?" This question has sparked debates across thousands of 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 type system—which is exactly why interviewers keep asking.
Table of Contents
- Type vs Interface Fundamentals Questions
- Declaration Merging Questions
- Unions and Advanced Types Questions
- Inheritance and Extension Questions
- Practical Usage Questions
- Quick Reference
Type vs Interface Fundamentals Questions
These questions test your understanding of the core differences between type aliases and interfaces.
What is the difference between type and interface in TypeScript?
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.
Think of interface as a contract or specification sheet—anything that implements this contract must have these properties and methods. A type alias 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.
// 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" };For simple object definitions, they behave the same way. The differences emerge when doing more complex operations.
When should you use type vs interface in TypeScript?
The choice depends on what you're trying to accomplish. Use interface for public APIs and object contracts that others will implement or extend. Use type when you need unions, mapped types, or other advanced features that interfaces don't support.
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
For simple object shapes with no special requirements, follow the existing codebase convention. Consistency matters more than the choice itself.
Can classes implement both interfaces and types?
Yes, classes can implement both interfaces and types, as long as the type represents an object structure. The behavior is identical in both cases.
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"; }
}The difference is that if you later need to augment the type through declaration merging, only the interface version will support that.
Declaration Merging Questions
These questions test your understanding of one of the key differentiators between interfaces and types.
What is declaration merging in TypeScript?
Declaration merging is a TypeScript feature where multiple interface declarations with the same name are automatically combined into a single interface containing all properties. This is unique to interfaces—types cannot do this.
// 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
};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.
If you try to declare a type with the same name twice, TypeScript gives an error:
type Config = { apiUrl: string };
type Config = { timeout: number }; // Error: Duplicate identifier 'Config'How do you extend third-party library types using declaration merging?
Declaration merging is invaluable when working with third-party libraries. You can extend their types without modifying the library's code by declaring an interface with the same name.
// 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" });This is commonly used to add custom properties to global objects, extend Express request/response objects, or augment library configuration interfaces.
// Extending a 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.
Unions and Advanced Types Questions
These questions test your understanding of type-only features that interfaces cannot replicate.
Why can't interfaces represent union types?
Interfaces are designed specifically for describing object shapes—they define a contract that objects must satisfy. Union types represent "either this OR that," which is a fundamentally different concept that interfaces weren't designed to express.
// 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 is a union of string literals, meaning a variable of type Status can only be one of those three specific strings. This is powerful for creating type-safe enumerations without the runtime overhead of TypeScript enums.
How do you create mapped types and conditional types?
Types excel at creating utility types and working with mapped types—operations that transform one type into another based on rules.
// 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"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.
How do you create discriminated union types for API responses?
Discriminated unions combine union types with a common "discriminator" property that TypeScript uses for type narrowing. This pattern is only possible with types.
// Use types for 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;
}
// 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;
}
}This pattern uses a type for ApiResponse because it requires a discriminated union, combined with interfaces for specific data shapes.
Inheritance and Extension Questions
These questions test your understanding of how interfaces and types handle composition and inheritance.
How do interfaces inherit using the extends keyword?
Interfaces use the extends keyword for inheritance, which should feel familiar from object-oriented programming. An interface can extend one or multiple other interfaces.
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
};Dog extends Animal, inheriting its properties while adding its own. HouseDog then extends both Dog and Pet, demonstrating multiple inheritance which interfaces support naturally.
How do types compose using intersection?
Types use intersection (&) for combining types. The result is functionally equivalent to interface extension, but with a subtle semantic difference.
type Animal = {
name: string;
age: number;
};
type Dog = Animal & {
breed: string;
bark(): void;
};
type Pet = {
owner: string;
};
type HouseDog = Dog & Pet & {
isHouseTrained: boolean;
};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.
Can interfaces extend types and vice versa?
Yes, interfaces and types are interoperable. Interfaces can extend types (if the type represents an object shape), and types can intersect with interfaces.
// 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;Understanding that interfaces and types are interoperable, not competing features, demonstrates deeper TypeScript knowledge.
Practical Usage Questions
These questions test your ability to apply type vs interface knowledge in real scenarios.
Which is more performant: type or interface?
Interfaces are slightly faster for the TypeScript compiler to process because 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. It's why the TypeScript team recommends interfaces when you don't need type-specific features, but both are perfectly acceptable for everyday use.
How do you create a type-safe event system?
This problem demonstrates practical understanding of when to use types vs interfaces together.
// Define events using a type (because we need 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;
}
// Usage - fully type-safe!
const emitter: TypedEventEmitter<EventMap> = /* implementation */;
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 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.
How do you create a utility type that makes some properties required?
This requires mapped types and conditional logic, making type the only choice.
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 }The RequireOnly utility type makes all properties optional except for the specified keys. This is a common pattern for form inputs where some fields are required and others are optional.
How do you handle team disagreements on type vs interface conventions?
This is a process question disguised as a technical one. The answer is to establish a clear guideline in the project's style guide: "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.
// .eslintrc.json
{
"rules": {
"@typescript-eslint/consistent-type-definitions": ["error", "interface"]
}
}This rule enforces interfaces for object shapes, while allowing types for unions and other type-only features.
Quick Reference
| 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 |
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
Related Articles
- 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
