TypeScript's type system fundamentally relies on Structural Typing (Duck Typing): type compatibility is determined solely by their shape (set of properties and methods), not by explicit nominal declaration (as in Java or C#). If two different types have the same field structure, the compiler considers them mutually interchangeable.
TypeScript provides two primary tools for describing the shapes of structural data:
interface): Open contracts for describing objects and classes. They support inheritance via extends and the automatic Declaration Merging mechanism.type): Flexible syntactic aliases for any entity in the type system (objects, primitives, union types, intersection types, tuples, and function signatures).In modern development, the choice between interface and type is dictated by the architectural context: interfaces are ideal for extensible public contracts of libraries and OOP hierarchies, while type aliases are indispensable for creating complex compositional and computed types.
The difference between interface and type boils down to openness (open for merging) versus fixation (immutable definition):
+-------------------------------------------------------------------------+
| INTERFACE vs TYPE: MENTAL MODEL |
+-------------------------------------------------------------------------+
| INTERFACE (Open Contract): |
| interface User { id: string } |
| interface User { name: string } ---> Result: { id: string; name: string }|
| (Declaration Merging works automatically) |
+-------------------------------------------------------------------------+
| TYPE ALIAS (Closed Alias): |
| type User = { id: string } |
| type User = { name: string } ---> ERROR TS2300: Duplicate identifier|
| (Redeclaration is strictly forbidden) |
+-------------------------------------------------------------------------+
| COMPOSITION CAPABILITIES: |
| type Role = 'admin' | 'user' ---> Interface CANNOT create a union! |
| type Pair = [number, string] ---> Interface CANNOT create a tuple! |
+-------------------------------------------------------------------------+&)
The compiler matches the list of required properties of the target type against the actual value.
For each property, the compatibility of its value types is recursively checked.
For direct object literals, the compiler blocks unknown fields, preventing typos.
The result of type relationship checks is stored in the compiler's internal cache to speed up compilation.
What happens if two interfaces with the same name are declared in different project files: `interface Window { analytics: Tracker }`?
process.env in Node.js or the global window object through interface merging.req.user to the base request interface.IUserRepository) to implement the Dependency Inversion pattern.type TenantEntity = BaseEntity & Auditable & TenantBound).extends vs &#Interfaces are extended using the extends keyword, while types are combined using the & intersection operator:
// 1. Interfaces: inheritance via extends
interface Identified {
id: string;
}
interface User extends Identified {
email: string;
role: 'admin' | 'user';
}
// 2. Type Aliases: composition via intersection (&)
type Timestamped = {
createdAt: Date;
updatedAt: Date;
};
type FullUser = User & Timestamped;
const activeUser: FullUser = {
id: 'usr_9482',
email: '[email protected]',
role: 'admin',
createdAt: new Date(),
updatedAt: new Date(),
};Interfaces strictly check the compatibility of overridden properties. If incompatible, the compiler immediately throws an error. In the case of type &, the compiler creates a never type for the conflicting property:
interface BaseNode {
id: string;
}
// ERROR: interface explicitly forbids incompatible redefinition
// interface NumberNode extends BaseNode {
// id: number; // TS2430: Interface 'NumberNode' incorrectly extends interface 'BaseNode'.
// }
// In Type Alias, intersecting incompatible primitives creates 'never':
type Alpha = { id: string };
type Beta = { id: number };
type Combined = Alpha & Beta;
// The 'id' field has type: string & number === never!
// const bad: Combined = { id: 123 }; // ERROR: Type 'number' is not assignable to type 'never'.type Can Do, But interface Cannot#Type aliases can represent primitives, tuples, unions, and computed properties (Mapped Types):
// Unions
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
// Tuples
type Coordinate = [latitude: number, longitude: number];
// Primitive Aliases
type UUID = string;
// Indexed Access Types
type UserRole = User['role']; // 'admin' | 'user'
// Mapped Types - forbidden inside an interface!
type ReadonlyRecord<T> = {
readonly [K in keyof T]: T[K];
};Why does the expression `const user: User = { id: '1', email: '[email protected]', extra: true }` cause an error, but when passed through an intermediate variable `const temp = { id: '1', email: '[email protected]', extra: true }; const user: User = temp;` the compiler remains silent?
If two different teams declare a UserConfig interface in the global scope, TypeScript will merge their fields together. This can mask hidden bugs and collisions.
Solution: Use type for internal structures or wrap interfaces in named ES modules with export.
When blindly merging types via &, it's easy to create properties of type never:
type ResA = { status: 200; data: string };
type ResB = { status: 404; error: string };
type Res = ResA & ResB; // The 'status' field became (200 & 404) === never!For mutually exclusive states, always use a union (|), not an intersection (&).