Last 30 Days
No notifications
Interfaces and type aliases are how you describe the *shape* of an object in TypeScript. They look almost identical, but each shines in different situations. Knowing when to use which (and how extends and & differ) is the difference between clean code and tangled type errors.
type gives a name to *any* type — primitive, union, tuple, object, function.
type ID = string | number;
type Point = { x: number; y: number };
type Handler = (e: Event) => void;interface is specialised for object shapes (and callable / constructable types).
interface User {
id: number;
name: string;
email: string;
}Used the same way:
const u: User = { id: 1, name: "Ada", email: "ada@x.com" };interface User {
id: number;
readonly createdAt: Date; // can't be reassigned after creation
email?: string; // may be omitted
}const u: User = { id: 1, createdAt: new Date() };
// u.createdAt = new Date(); // ❌
u.email = "x@y.com"; // ✅
> readonly is a *compile-time* check only. Nothing prevents JS from mutating at runtime.
interface StringDict {
[key: string]: string;
}const env: StringDict = {};
env.HOME = "/home/rahul"; // ✅
// env.PORT = 3000; // ❌ number not assignable to string
interface Counter {
increment(): number; // method shorthand
reset: () => void; // property holding a function
}For most code these behave the same. Method shorthand is bivariant — looser checking around this — so if you turn on strictFunctionTypes (it's part of strict), prefer the property form for stricter callbacks.
interface Animal { name: string }
interface Dog extends Animal {
breed: string;
}const d: Dog = { name: "Rex", breed: "Lab" };
Multiple parents are allowed:
interface Swimmer { swim(): void }
interface Flyer { fly(): void }
interface Duck extends Animal, Swimmer, Flyer {}&The type equivalent of extends:
type Animal = { name: string };
type Dog = Animal & { breed: string };Intersections can combine anything, including unions:
type Status = "on" | "off";
type Strict = Status & string; // still "on" | "off"But they fail loudly when properties conflict:
type A = { x: string };
type B = { x: number };
type C = A & B; // C.x is never — impossible to satisfyTwo interfaces with the same name merge. Type aliases throw a duplicate-identifier error.
interface User { id: number }
interface User { name: string }
// User now has id AND nameThis is what lets libraries (and you!) extend Express's Request or augment global types:
declare global {
interface Window { myApp: { version: string } }
}| Need | Pick |
| Public object/class shape | interface |
| Union, tuple, primitive alias, conditional type | type |
| Library API you might want others to extend | interface (for declaration merging) |
| Quick local shape with a function in it | either |
A useful default: interface for objects, type for everything else. Don't agonise — they're 95 % equivalent.
Object literals get an extra check that catches typos:
interface Options { url: string; method?: string }function fetchIt(opts: Options) {}
fetchIt({ url: "/api", methid: "GET" });
// ^^^^^^^ ❌ Object literal may only specify known properties
But assigning through a variable bypasses it:
const o = { url: "/api", methid: "GET" }; // typed as { url: string; methid: string }
fetchIt(o); // ❌ different reason — methid not in OptionsTo explicitly allow extra props, add an index signature or use as Options.
implementsA class can promise to satisfy an interface:
interface Greeter { greet(): string }class Hello implements Greeter {
greet() { return "hi"; }
}
If the class is missing a member, the compiler complains.