Junior 4 min · March 05, 2026

`type` Broke Library Augmentation - Use Interfaces

A production bug: type aliases block library augmentation, causing TypeScript errors and any casts.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • TypeScript's type and interface both define object shapes but serve different purposes.
  • interface shines for domain entities — supports extends, implements, and declaration merging.
  • type is your tool for unions, tuples, function signatures, and mapped types.
  • Performance difference is negligible at compilation; choosing wrong leads to maintenance debt.
  • Biggest production mistake: using type when you need declaration merging for library augmentation.
Plain-English First

Think of a type or interface like a job description at a company. Before you hire someone, you write down exactly what skills and responsibilities that role requires — 'must know JavaScript, must handle billing, must have an email address.' TypeScript types and interfaces do the same thing for your data. They say: 'any object that wants to be a User must have a name, an email, and an age.' The difference between them is a bit like the difference between a sticky note (type) and an official HR form (interface) — both describe the role, but the HR form can be updated and extended department by department, while the sticky note is a fixed snapshot.

You've seen both type and interface in TypeScript codebases. They look interchangeable — both can describe an object's shape. But they aren't. Picking the wrong one causes real pain: you'll hit compile errors when trying to implements a type, or you'll be stuck when a third-party library expects to merge declarations and yours are sealed. Understanding the difference isn't academic trivia — it's the difference between ten-line workarounds and clean, maintainable types. This article lays out the concrete rules, the edge cases that bite you in production, and a decision framework you can use today.

Type Aliases — Snapshots of Any Shape You Can Imagine

A type alias does exactly what its name says: it gives a name to any type expression. That's broader than it sounds. You can alias a primitive, a union, a tuple, a function signature, or a complex object — anything TypeScript can express, you can name with type.

This is where type shines over interface: flexibility. An interface can only describe an object shape. A type can describe 'a string OR a number', a 'function that takes two numbers and returns a boolean', or even 'a tuple where position 0 is always a string and position 1 is always a Date.' You simply can't express those ideas with interface alone.

In a real codebase you'll use type aliases constantly for union types — things like an API response that can be either a success payload or an error object. You'll also use them to document function signatures so every developer on your team knows exactly what a callback or handler is supposed to look like before they write a single line.

typeAliasExamples.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// ── 1. Aliasing a union type ──────────────────────────────────────────────
// This says: a Status can ONLY be one of these three strings.
// TypeScript will error if you try to assign anything else.
type Status = 'idle' | 'loading' | 'success' | 'error';

// ── 2. Describing a function signature ────────────────────────────────────
// Anyone reading this immediately knows what this callback must look like.
// No more guessing what arguments an event handler receives.
type PriceFormatter = (amount: number, currencyCode: string) => string;

const formatUSD: PriceFormatter = (amount, currencyCode) => {
  // Intl.NumberFormat gives us locale-aware currency formatting
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: currencyCode,
  }).format(amount);
};

console.log(formatUSD(1999.5, 'USD')); // $1,999.50

// ── 3. Describing a complex object shape ──────────────────────────────────
type ApiResponse<T> = {
  data: T | null;       // the actual payload, or null if there's an error
  status: Status;       // reusing our union type from above
  errorMessage?: string; // the ? makes this property optional
  requestedAt: Date;
};

// A concrete usage — an API response that carries a list of product names
const productResponse: ApiResponse<string[]> = {
  data: ['Wireless Keyboard', 'USB Hub', 'Monitor Stand'],
  status: 'success',
  requestedAt: new Date('2024-06-01T10:00:00Z'),
};

console.log(productResponse.status);       // success
console.log(productResponse.data?.length); // 3

// ── 4. Tuple type — positional, fixed-length array ────────────────────────
// Use this when the ORDER and TYPES of positions are meaningful.
// Here: [latitude, longitude] — never the other way around.
type Coordinates = [latitude: number, longitude: number];

const sydneyHarbour: Coordinates = [-33.8568, 151.2153];
console.log(`Lat: ${sydneyHarbour[0]}, Lng: ${sydneyHarbour[1]}`);
// Lat: -33.8568, Lng: 151.2153
Output
$1,999.50
success
3
Lat: -33.8568, Lng: 151.2153
Pro Tip:
When you need to express 'this value can be one of several specific things' — a union — always reach for type. Interfaces can't model unions. A type Status = 'idle' | 'loading' is one of the most powerful patterns in TypeScript for eliminating impossible states from your app.
Production Insight
Using type for unions prevents entire classes of bugs where invalid states slip through.
In a payment system, a union type PaymentState = 'pending' | 'completed' | 'failed' makes it impossible to accidentally assign an invalid state.
The compiler enforces it — no runtime check needed.
Key Takeaway
Reach for type when you need to describe what interface cannot — unions, tuples, primitives, function signatures.
type is the only option for non-object shapes.

Interfaces — Contracts That Can Grow and Merge

An interface describes the shape of an object and nothing else — but it does that job exceptionally well. Its real superpower is something called declaration merging: if you declare the same interface name twice, TypeScript merges them into one. This sounds niche until you realise it's the mechanism behind every third-party library that lets you extend their types from your own code, without touching the library source.

Interfaces also support extends, which works just like class inheritance. You can build a hierarchy of contracts — a BaseEntity with id and createdAt, extended by User which adds email, extended further by AdminUser which adds permissions. This makes your type system document your domain model in a way that mirrors how you'd describe it to a new teammate.

For almost any object shape that represents a domain entity — User, Product, Order, Invoice — interface is the idiomatic TypeScript choice. When you're describing something your app will create many instances of, or something that other parts of the codebase will extend or implement, interface communicates that intent clearly.

interfaceExamples.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// ── 1. Base interface for any entity stored in the database ───────────────
interface BaseEntity {
  id: string;           // UUID from the database
  createdAt: Date;
  updatedAt: Date;
}

// ── 2. Extending an interface — User inherits all of BaseEntity ───────────
// The extends keyword says: a User must satisfy BaseEntity PLUS these fields.
interface User extends BaseEntity {
  email: string;
  displayName: string;
  isEmailVerified: boolean;
}

// ── 3. Multi-level extension ───────────────────────────────────────────────
// AdminUser must satisfy User (which itself satisfies BaseEntity) plus this.
interface AdminUser extends User {
  permissions: string[];  // e.g. ['users:delete', 'billing:read']
  lastLoginAt: Date;
}

// ── 4. Declaration merging — extending an interface from a separate location
// Imagine this second block is in a different file, e.g. analytics.d.ts
// TypeScript merges BOTH declarations into one complete interface.
interface User {
  analyticsId?: string; // added by your analytics module — optional
}

// Now a User object can legally include analyticsId
const currentUser: User = {
  id: 'usr_8f3kd92',
  createdAt: new Date('2023-01-15'),
  updatedAt: new Date('2024-05-20'),
  email: 'priya@example.com',
  displayName: 'Priya Sharma',
  isEmailVerified: true,
  analyticsId: 'ga_4829xz', // merged from second declaration
};

console.log(currentUser.displayName);  // Priya Sharma
console.log(currentUser.analyticsId);  // ga_4829xz

// ── 5. Interface for a class contract ────────────────────────────────────
// This is where interface is strictly more appropriate than type.
// The implements keyword enforces the contract at the class level.
interface NotificationService {
  send(recipient: string, message: string): Promise<void>;
  getDeliveryStatus(messageId: string): Promise<'delivered' | 'failed' | 'pending'>;
}

class EmailNotificationService implements NotificationService {
  async send(recipient: string, message: string): Promise<void> {
    // Real implementation would call an email API here
    console.log(`Sending email to ${recipient}: "${message}"`);
  }

  async getDeliveryStatus(messageId: string): Promise<'delivered' | 'failed' | 'pending'> {
    // Real implementation would query an email delivery API
    console.log(`Checking status for message: ${messageId}`);
    return 'delivered';
  }
}

const emailService = new EmailNotificationService();
emailService.send('alex@example.com', 'Your order has shipped!');
// Sending email to alex@example.com: "Your order has shipped!"
Output
Priya Sharma
ga_4829xz
Sending email to alex@example.com: "Your order has shipped!"
Declaration Merging in the Wild:
Express.js uses declaration merging to let you add custom properties to the Request interface. You declare 'interface Request { currentUser?: User }' in a .d.ts file and suddenly req.currentUser is type-safe everywhere in your app — without modifying the Express source. It's one of the most elegant patterns in the TypeScript ecosystem.
Production Insight
Declaration merging is a double-edged sword. If you accidentally merge an interface with a property that already exists but with a different type, you get an error. Worse, if you augment a third-party interface and make the property required, existing code that doesn't provide it will break. Always add new properties as optional (?).
Key Takeaway
Use interface for objects others might extend — domain entities, component props, and class contracts.
Declaration merging is a superpower for library augmentation, but use it with care.

Intersection Types and Generics — Composing Complex Shapes

Once you understand types and interfaces individually, the real power comes from combining them. Intersection types (using &) let you merge multiple types into one that must satisfy all of them simultaneously. Think of it as 'this thing must be a User AND have these extra properties.' It's composition instead of inheritance.

Generics add a dimension of reusability that transforms your types from single-use descriptions into flexible templates. A generic type is like a function — it takes a type as a parameter and returns a new type. You've already seen this with ApiResponse<T> above. This is how TypeScript's own built-in utilities like Partial<T>, Required<T>, Pick<T, K> and Readonly<T> work under the hood.

In real-world code, you'll use these patterns every time you write shared utility functions, data fetching hooks, form handlers, or anything that needs to work with multiple different entity types. Getting comfortable with generics is the single biggest jump from 'TypeScript beginner' to 'TypeScript intermediate.'

intersectionAndGenerics.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// ── 1. Intersection type — combining two shapes into one ──────────────────
type Timestamped = {
  createdAt: Date;
  updatedAt: Date;
};

type Identifiable = {
  id: string;
};

// A Product must satisfy BOTH Timestamped AND Identifiable, plus its own fields
type Product = Timestamped & Identifiable & {
  name: string;
  priceInCents: number; // storing money as integers avoids floating-point bugs
  stockCount: number;
};

const wirelessMouse: Product = {
  id: 'prod_wm_001',
  name: 'Wireless Ergonomic Mouse',
  priceInCents: 4999, // represents $49.99
  stockCount: 142,
  createdAt: new Date('2024-01-10'),
  updatedAt: new Date('2024-06-01'),
};

console.log(`${wirelessMouse.name}: $${wirelessMouse.priceInCents / 100}`);
// Wireless Ergonomic Mouse: $49.99

// ── 2. Generic type — a reusable wrapper for paginated API responses ───────
// The <T> is a placeholder. When you USE this type, you fill in what T is.
type PaginatedResult<T> = {
  items: T[];           // an array of whatever type T turns out to be
  totalCount: number;
  currentPage: number;
  totalPages: number;
  hasNextPage: boolean;
};

// Now reuse it for any entity — no duplication
type PaginatedProducts = PaginatedResult<Product>;
type PaginatedUsers = PaginatedResult<{ id: string; email: string }>;

const productPage: PaginatedProducts = {
  items: [wirelessMouse],
  totalCount: 87,
  currentPage: 1,
  totalPages: 9,
  hasNextPage: true,
};

console.log(`Page ${productPage.currentPage} of ${productPage.totalPages}`);
// Page 1 of 9

// ── 3. Generic function — works with any entity that has an id ─────────────
// The constraint 'extends Identifiable' means: T must have at least an id.
// This prevents calling findById with a plain number or a string.
function findById<T extends Identifiable>(items: T[], targetId: string): T | undefined {
  // TypeScript knows items[i].id is safe because of the extends constraint
  return items.find(item => item.id === targetId);
}

const foundProduct = findById([wirelessMouse], 'prod_wm_001');
console.log(foundProduct?.name); // Wireless Ergonomic Mouse

// ── 4. Using built-in utility types ───────────────────────────────────────
// Partial<T> makes every property optional — perfect for update/patch payloads
type ProductUpdatePayload = Partial<Pick<Product, 'name' | 'priceInCents' | 'stockCount'>>;

// A PATCH request only needs to send the fields being changed
const priceUpdate: ProductUpdatePayload = {
  priceInCents: 3999, // just updating the price — all other fields are optional
};

console.log(priceUpdate); // { priceInCents: 3999 }
Output
Wireless Ergonomic Mouse: $49.99
Page 1 of 9
Wireless Ergonomic Mouse
{ priceInCents: 3999 }
Pro Tip:
Partial<Pick<T, 'field1' | 'field2'>> is one of the most useful type patterns in real codebases. Use it for HTTP PATCH payloads, form partial-save states, or any update operation where you only want to allow changing a subset of fields. It's type-safe AND self-documenting.
Production Insight
Generic types reduce duplication. Without them, you'd write separate PaginatedProduct, PaginatedUser, etc. When you later add a PaginatedOrder, you'd copy-paste. A single PaginatedResult<T> eliminates that. The performance impact is compile-time only — no runtime overhead.
Key Takeaway
Intersection types compose, generics reuse — master both to avoid type duplication.
Partial<Pick<T, K>> is a production-grade pattern for selective updates.

Type Guards — Making TypeScript Trust Your Runtime Logic

Here's a scenario that catches almost every developer moving from JavaScript to TypeScript: you have a variable typed as string | number — a union type. You want to call .toUpperCase() on it. TypeScript refuses, because what if it's a number? You need to prove to the compiler that at this specific point in the code, it's definitely a string.

Type guards are the mechanism for that proof. The simplest form is a typeof or instanceof check inside an if block — TypeScript understands these natively and narrows the type automatically inside that block. But when you're working with custom object types (not primitives), you need a user-defined type guard: a function that returns 'value is SomeType' in its signature.

This pattern is everywhere in production code — any time you receive data from an external source (an API, a user event, localStorage), you can't know the shape at compile time. Type guards are how you bridge the gap between 'unknown blob of JSON' and 'fully typed domain object.'

typeGuards.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// ── Our domain types ──────────────────────────────────────────────────────
type SuccessResponse = {
  kind: 'success'; // discriminant field — a literal type, not just string
  data: { orderId: string; total: number };
};

type ErrorResponse = {
  kind: 'error'; // different literal — same field name, different value
  message: string;
  code: number;
};

// A discriminated union — both members share 'kind' but with different values
type CheckoutResponse = SuccessResponse | ErrorResponse;

// ── 1. Discriminated union narrowing — the cleanest pattern ───────────────
// TypeScript uses the 'kind' field to automatically narrow the type in each branch.
function handleCheckoutResponse(response: CheckoutResponse): void {
  if (response.kind === 'success') {
    // Inside this block, TypeScript KNOWS response is SuccessResponse
    // So response.data is available and type-safe
    console.log(`Order confirmed! ID: ${response.data.orderId}`);
    console.log(`Total charged: $${response.data.total / 100}`);
  } else {
    // In the else block, TypeScript KNOWS response is ErrorResponse
    console.log(`Checkout failed [${response.code}]: ${response.message}`);
  }
}

handleCheckoutResponse({
  kind: 'success',
  data: { orderId: 'ord_7f4kx', total: 8997 },
});
// Order confirmed! ID: ord_7f4kx
// Total charged: $89.97

handleCheckoutResponse({
  kind: 'error',
  message: 'Card declined',
  code: 4001,
});
// Checkout failed [4001]: Card declined

// ── 2. User-defined type guard — for validating unknown external data ──────
// The return type 'value is SuccessResponse' is the magic.
// When this function returns true, TypeScript narrows the type in the calling scope.
function isSuccessResponse(value: unknown): value is SuccessResponse {
  // We manually check every field we care about
  return (
    typeof value === 'object' &&
    value !== null &&
    'kind' in value &&
    (value as SuccessResponse).kind === 'success' &&
    'data' in value
  );
}

// Simulating data coming back from fetch() — typed as unknown
const rawApiData: unknown = {
  kind: 'success',
  data: { orderId: 'ord_9g5kp', total: 4500 },
};

if (isSuccessResponse(rawApiData)) {
  // TypeScript now trusts that rawApiData is SuccessResponse
  console.log(`Validated order: ${rawApiData.data.orderId}`);
} else {
  console.log('Received unexpected response shape from API');
}
// Validated order: ord_9g5kp
Output
Order confirmed! ID: ord_7f4kx
Total charged: $89.97
Checkout failed [4001]: Card declined
Validated order: ord_9g5kp
Watch Out:
Discriminated unions only work when the discriminant field uses a literal type — a specific string like 'success', not just string. If you type kind as string, TypeScript can't narrow it because any string could be anything. Always use string literal types as your discriminant field.
Production Insight
Without proper type guards, you'll either litter your code with as assertions (lying to the compiler) or wrap everything in unsafe any. Both create runtime holes. A user-defined type guard costs a few microseconds per check — negligible compared to the safety gain.
Always guard external data with type predicates before using it as your domain type.
Key Takeaway
Use discriminated unions with literal discriminants for exhaustive type narrowing.
For external data, use user-defined type guards — never as assertions.

When to Choose type vs interface: A Production Decision Guide

By now you know the capabilities of each. The real question is: which one do you actually write? The answer depends on the role that type plays in your codebase.

Start with interface for object shapes that represent domain entities — User, Product, Order. These are the backbone of your business logic. They benefit from extends, implements, and the ability for other modules to augment them. Interface communicates 'this is a contract'.

Use type for everything else: unions (Status = 'idle' | 'loading'), tuples ([lat, lng]), function signatures ((id: string) => Promise<User>), mapped types, utility types (Nullable<T>), and any composition involving intersections or generics. Type communicates 'this is a composition' or 'this is a variant'.

When in doubt, ask: 'Could another part of the system need to add a property to this?' If yes, interface. If no, type works fine. There's rarely a wrong choice for a pure object shape — both work — but interface gives you room to grow.

decisionGuide.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// ── Production rule of thumb ─────────────────────────────────────────────
// Domain entity → interface
interface User {
  id: string;
  email: string;
}

// Utility/union → type
type UserUpdatePayload = Partial<Pick<User, 'email'>>;

// Function type → type
type UserFetcher = (id: string) => Promise<User>;

// Class contract → interface
interface Repository<T> {
  find(id: string): Promise<T | undefined>;
  save(entity: T): Promise<void>;
}

// Works both ways, but prefer interface for extensibility
class UserRepository implements Repository<User> {
  async find(id: string): Promise<User | undefined> {
    // ...
  }
  async save(entity: User): Promise<void> {
    // ...
  }
}

// ── When you need both: type for union, interface for base shape ──────────
interface BaseAPIResult {
  timestamp: Date;
}

type APIResult<T> = BaseAPIResult & {
  data: T | null;
  error: string | null;
};

// This pattern is common — interface for the fixed contract, type for the variant part.
Mental Model: Interface as Contract, Type as Formula
  • Interface: like a legal contract — you sign it, and others can add clauses via declaration merging.
  • Type: like a mathematical formula — it takes inputs (generics) and produces a single, immutable result.
  • If you need to enforce a shape across your team and allow future extension, write an interface.
  • If you need to express a complex transformation or a variant, write a type alias.
Production Insight
In a large codebase, mixed usage without guidelines creates friction. A team that uses type for domain objects hits issues when they need to augment with analytics. A team that uses interface for everything is forced to write messy workarounds for unions. Establish a convention early: object contracts → interface, everything else → type.
The cost of refactoring later is higher than the cost of choosing right the first time.
Key Takeaway
If it can be an interface, make it one — it's more flexible.
If it can't, use type — that's exactly what it's for.
Default: interface for objects, type for variants and utilities.
Decision Tree: type or interface?
IfObject shape that may be extended by other modules
UseUse interface — supports declaration merging.
IfObject shape that a class will implement
UseUse interface — implements only works with interface.
IfUnion type (A | B) or intersection (A & B) of multiple types
UseUse type — interface cannot express unions natively.
IfTuple or function signature
UseUse type — interface cannot define tuples or call signatures directly.
IfMapped type or conditional type (e.g., Partial<T>)
UseUse type — interface syntax doesn't support these.
● Production incidentPOST-MORTEMseverity: high

The `type` That Broke Library Augmentation

Symptom
TypeScript errors everywhere after adding a library that expected to augment props. The team resorted to any casts to silence the compiler.
Assumption
The team assumed type and interface were equivalent for object types, so they standardized on type for consistency.
Root cause
type aliases are closed — they cannot be merged by subsequent declarations. The third-party library's module augmentation added properties to an interface, but the original type was invisible to it.
Fix
Replaced the type with an interface (no code change needed — same shape). The declaration merging worked immediately, and all any casts were removed.
Key lesson
  • Use interface for any type that might need augmentation — especially public APIs and component props.
  • type is fine for internal unions, tuples, and utility types that don't need external extension.
  • If you're unsure, prefer interface for object shapes; you can always use a type alias for the same shape later if needed.
Production debug guideQuick resolution for common type/interface missteps in production.4 entries
Symptom · 01
Cannot write class Foo implements MyType — TypeScript error: 'Only interfaces can be implemented'.
Fix
Change MyType from type to interface. Classes can only implement interfaces.
Symptom · 02
Trying to add a property to a type via declaration merging gets 'Duplicate identifier' error.
Fix
Refactor the type to an interface. Only interfaces support declaration merging.
Symptom · 03
Property does not exist on union type – TypeScript won't narrow even after checks.
Fix
Ensure the union uses a discriminant with a literal type (e.g., kind: 'success' | 'error'), not a general string.
Symptom · 04
Can't create a union type with interface – error: 'An interface can only extend an object type or intersection of object types'.
Fix
Use type for unions. Interface cannot express string | number or {a:1} | {b:2}.
★ Quick Decision: type vs interfaceMatch your use case to the right construct.
Need declaration merging (augment a type from another file/library)
Immediate action
Use `interface`
Commands
interface Props { x: number }
// In another file: interface Props { y: string } // merges
Fix now
If current code uses type, replace with interface and keep the shape identical.
Need a union type (`string | number` or a discriminated union)+
Immediate action
Use `type`
Commands
type Result = Success | Error
// Interface can't represent unions
Fix now
If you have an interface that you want to union, you must refactor to type.
Need a tuple (fixed-length array with specific types per index)+
Immediate action
Use `type`
Commands
type Point = [x: number, y: number]
// Interface cannot define tuple members
Fix now
Change the tuple definition from interface to type.
Creating a contract for a class (`implements`)+
Immediate action
Use `interface`
Commands
interface Service { execute(): void }
class MyService implements Service { ... }
Fix now
If you have a type that a class implements, change it to interface.
Feature / Capability
Feature / Capabilitytype aliasinterface
Describe an object shapeYesYes
Union types (A | B)Yes — primary use caseNo — not supported
Intersection / mergingYes — using &Yes — using extends
Declaration mergingNo — causes a compile errorYes — core feature
Extend / implement in a classNo — classes can't implement type aliasesYes — implements keyword
Tuple typesYesNo
Function signaturesYes — first-class citizenYes — but more verbose
Generic parametersYesYes
Computed property keysYes — via mapped typesLimited support
Recommended for domain entitiesSituationalYes — idiomatic choice
Recommended for utility/union typesYes — idiomatic choiceNo

Key takeaways

1
Use interface for domain object shapes and class contracts
it supports extends, implements, and declaration merging, which makes it the right tool for anything your codebase will build on top of.
2
Use type for unions, tuples, function signatures, and utility compositions
these are things interface simply cannot express, so type isn't just a preference here, it's the only option.
3
Discriminated unions with a literal-typed 'kind' or 'type' field are one of the most powerful TypeScript patterns
they give you exhaustive type narrowing and make impossible states genuinely impossible.
4
Generic types transform single-use type descriptions into reusable templates. Mastering PaginatedResult<T>, ApiResponse<T>, and utility types like Partial<Pick<T, K>> is what separates TypeScript users from TypeScript developers.
5
When in doubt, default to interface for objects and type for everything else. That convention scales well in teams.

Common mistakes to avoid

3 patterns
×

Using `type` for class contracts

Symptom
The TypeScript compiler gives an odd error when you try to write class UserService implements UserServiceType because classes can only implement interfaces, not arbitrary type aliases that contain union types or mapped types.
Fix
Always use interface when you're describing a contract that a class will implement. Reserve type for unions, tuples, primitives, and utility compositions.
×

Forgetting that declaration merging can cause unexpected behaviour

Symptom
You add a property to a third-party interface via a .d.ts file, and suddenly that property appears as required on objects throughout your codebase causing TypeScript errors in places you didn't touch.
Fix
Always mark augmented properties as optional (using ?) when extending third-party interfaces, since you can't guarantee existing code provides that property.
×

Using `as` type assertions instead of type guards to handle unknown data

Symptom
You write const user = apiResponse as User and TypeScript stops complaining, but you get a runtime crash when the API returns unexpected data because you bypassed the type checker rather than validating the data.
Fix
Use a user-defined type guard function (returning 'value is User') to actually validate the data structure at runtime. Type assertions are a promise to the compiler, not a runtime check — if you lie, TypeScript can't save you.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What's the practical difference between a type alias and an interface in...
Q02SENIOR
Explain declaration merging in TypeScript interfaces. Where have you use...
Q03SENIOR
What is a discriminated union and how does it relate to type narrowing? ...
Q04JUNIOR
Can a type alias extend an interface? How would you do it?
Q01 of 04SENIOR

What's the practical difference between a type alias and an interface in TypeScript, and can you give a real scenario where you'd choose one over the other?

ANSWER
The key differences: interfaces support declaration merging and can be extended via extends and implemented via implements; type aliases cannot be merged or implemented, but they can represent unions, tuples, and more complex type expressions. A real scenario: if you're defining the shape of a React component's props that other libraries might augment (like react-router's RouteComponentProps), use an interface. If you're writing a union type like type Result = 'success' | 'error', use a type.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Should I use type or interface for objects in TypeScript?
02
Can a TypeScript type alias extend an interface?
03
What does it mean when people say TypeScript interfaces are 'open' and type aliases are 'closed'?
04
Can I use declaration merging with type aliases?
05
When should I use intersection types (&) vs interface extends?
🔥

That's TypeScript. Mark it forged?

4 min read · try the examples if you haven't

Previous
Introduction to TypeScript
2 / 15 · TypeScript
Next
TypeScript Classes and OOP