03 - Interfaces & Types
Interfaces
Interfaces define the shape of objects — what properties they must have and what types those properties are. They only exist at compile time and produce zero JavaScript output.
interface User {
id: number;
name: string;
email?: string; // optional — doesn't have to be provided
readonly createdAt: Date; // readonly — can't be changed after creation
}
const user: User = {
id: 1,
name: 'Alice',
createdAt: new Date()
// email is optional, so we can skip it
};
user.createdAt = new Date(); // ❌ error — readonlyMethod Signatures in Interfaces
Interfaces can define what methods an object must have — but only the signature (name, parameters, return type). Never the implementation.
interface UserService {
getUser(id: number): Promise<User>; // ✅ signature only
deleteUser(id: number): Promise<void>; // ✅ signature only
// getUser(id: number) { return fetch(...); } // ❌ can't put code in an interface
}
// The interface just says "you must have these methods."
// How you implement them is up to you — a plain object works:
const service: UserService = {
async getUser(id) { return fetch(`/api/users/${id}`).then(r => r.json()); },
async deleteUser(id) { await fetch(`/api/users/${id}`, { method: 'DELETE' }); }
};Extending Interfaces
An interface can inherit from another and add more properties:
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// Dog requires both name (from Animal) and breed
const myDog: Dog = {
name: 'Buddy',
breed: 'Golden Retriever'
};Declaration Merging
If you define the same interface name twice, TypeScript merges them. This is unique to interfaces — types can't do this.
interface Window {
customProp: string;
}
interface Window {
anotherProp: number;
}
// Window now has both customProp and anotherPropThis is mainly useful for extending third-party types (like adding properties to the global Window).
Type Aliases
type is a TypeScript keyword — types are checked at compile time and completely erased from the JavaScript output. They're more flexible than interfaces and can represent any type, not just objects.
type Status = 'active' | 'inactive' | 'pending'; // union of strings
type ID = string | number; // union of types
type Point = {
x: number;
y: number;
};Function Types
Types can describe function signatures:
type Formatter = (input: string) => string;
const shout: Formatter = (input) => input.toUpperCase();
const whisper: Formatter = (input) => input.toLowerCase();Extending Types with Intersection (&)
Types use & instead of extends to combine:
type A = { a: number };
type B = { b: string };
type C = A & B; // has both a and b
const obj: C = { a: 1, b: 'hello' };Union Types
A value that can be one of several types.
type ID = string | number;
function printId(id: ID) {
if (typeof id === 'string') {
console.log(id.toUpperCase()); // TypeScript knows it's a string here
} else {
console.log(id); // TypeScript knows it's a number here
}
}Discriminated Unions
Add a shared type property to tell variants apart. TypeScript narrows the type automatically inside if checks:
interface Success {
type: 'success';
data: string;
}
interface Error {
type: 'error';
message: string;
}
type Result = Success | Error;
function handleResult(result: Result) {
if (result.type === 'success') {
console.log(result.data); // ✅ TypeScript knows this is Success
} else {
console.log(result.message); // ✅ TypeScript knows this is Error
}
}Here type is the discriminator — a shared property that acts like a label. When you have a union of different object shapes, how does TypeScript know which one you're dealing with? You give each variant a shared property with a unique literal value — a "tag" that tells them apart. When you check result.type === 'success', TypeScript reads that label and knows the object must be Success, so it lets you access .data. In the else branch, it knows it's Error, so .message is available.
String Literal Unions vs Enums
String literal unions and enums solve the same problem — restricting a value to specific options. Unions are preferred in most TypeScript code.
// Union — just a type, erased at compile time (preferred)
type PaymentMethod = 'credit_card' | 'paypal' | 'crypto';
// Enum — generates real JavaScript (less common)
enum PaymentMethod {
CreditCard = 'credit_card',
PayPal = 'paypal',
Crypto = 'crypto'
}| Union | Enum | |
|---|---|---|
| Runtime existence | ❌ erased | ✅ real JS object |
| Bundle size | None | Adds code |
| Loop over all values | ❌ can't | ✅ Object.values(PaymentMethod) |
| Rename a value | Manual find/replace | Change in one place |
| Autocomplete | ✅ | ✅ |
// Union — you use the raw string
let method: PaymentMethod = 'credit_card';
// Enum — you reference the enum name
let method = PaymentMethod.CreditCard;Use unions by default. Use enums when you need to iterate over all values (e.g., rendering a dropdown) or want centralized renaming.
type PaymentMethod = 'credit_card' | 'paypal' | 'crypto';
function processPayment(method: PaymentMethod, amount: number) {
// TypeScript ensures only valid payment methods are passed
}
processPayment('credit_card', 50); // ✅
processPayment('cash', 50); // ❌ errorInterface vs Type — When to Use Which
Both can define object shapes. The differences:
| Interface | Type | |
|---|---|---|
| Object shapes | ✅ | ✅ |
extends |
✅ extends |
✅ & (intersection) |
| Declaration merging | ✅ Re-declare to extend | ❌ |
Unions (A | B) |
❌ | ✅ |
| Primitives / tuples | ❌ | ✅ |
| Function signatures | Verbose | Clean |
Rule of thumb:
- Use
interfacefor object shapes and data models (most common) - Use
typefor unions, function types, and anything that isn't a plain object shape - Either works for objects — pick one and be consistent
Key Takeaways
- Interfaces define object shapes — compile-time only, no JavaScript output
?= optional property,readonly= can't be changed- Interfaces can have method signatures but never implementations
- Types are more flexible — unions, intersections, function types, primitives
- Discriminated unions use a shared
typeproperty for type narrowing interface extendsvstype &— both combine types, different syntax- Declaration merging is unique to interfaces (useful for extending third-party types)
- Default to
interfacefor objects,typefor everything else