"Interchangeable" has one sharp exception. A type alias for an object shape is assignable to Record<string, string>, because TypeScript treats it as having an implicit index signature, while an interface with exactly the same properties is not, because interfaces can be extended later by declaration merging, so TypeScript won't assume it knows every key. The same object literal passes or fails depending only on which keyword declared its type.
interface UserI { name: string; city: string }
type UserT = { name: string; city: string };
function toQuery(params: Record<string, string>): string {
return new URLSearchParams(params).toString();
}
const a: UserT = { name: "Asha", city: "Pune" };
const b: UserI = { name: "Ravi", city: "Delhi" };
console.log(toQuery(a)); // fine
console.log(toQuery(b));
// error TS2345: Argument of type 'UserI' is not assignable to parameter of type 'Record<string, string>'.
// Index signature for type 'string' is missing in type 'UserI'.Both interface and type describe an object's shape, and for most everyday use they're interchangeable — a value satisfying one will satisfy the equivalent other. The genuine, load-bearing difference is declaration merging: multiple interface declarations with the same name automatically combine into one, which type aliases cannot do at all (redeclaring the same type name is a compile error). Conversely, only type can directly name a union, intersection, tuple, or primitive — interface is limited to object shapes.
Both describe an object shape, nearly identically
These two produce equivalent type-checking behavior — a value assignable to one is assignable to the other.
interface UserI {
name: string;
age: number;
}
type UserT = {
name: string;
age: number;
};
const u1: UserI = { name: "Alice", age: 30 }; // fine
const u2: UserT = { name: "Bob", age: 25 }; // also fineSo "nearly identically" above is precise: the difference is index-signature compatibility, and it follows directly from declaration merging below. A type alias is closed, so TypeScript can see all of its keys; an interface is open, so it can't. When an interface-typed value has to go into something keyed by arbitrary strings, either declare the shape as a type, or pass a copy, which TypeScript types as a fresh object literal:
interface UserI { name: string; city: string }
function toQuery(params: Record<string, string>): string {
return new URLSearchParams(params).toString();
}
const b: UserI = { name: "Ravi", city: "Delhi" };
console.log(toQuery({ ...b })); // name=Ravi&city=DelhiDeclaration merging: only interface can do this
This is genuinely useful for extending a type defined elsewhere (a library's global types, for example) — and it's the one thing a type alias fundamentally cannot replicate.
interface Window {
title: string;
}
interface Window { // a SECOND declaration of the same name
ts: number;
}
// they automatically merge into one interface with BOTH properties:
// interface Window { title: string; ts: number; }
// type Window = { title: string };
// type Window = { ts: number }; // COMPILE ERROR: Duplicate identifier 'Window'Only type can name a union, intersection, or primitive
interface is restricted to describing object shapes — it cannot directly name a union of string literals, a primitive alias, or a tuple the way type can.
type Status = "active" | "inactive"; // fine — type can name a union directly
// interface Status = "active" | "inactive"; // not valid syntax at all
type ID = string | number; // fine
type Coordinates = [number, number]; // fine — a tuple