Interfaces in TypeScript define contracts for object shapes, specifying what properties and methods an object must have without dictating how they're implemented. They provide compile-time type checking that ensures objects conform to expected structures, catching shape mismatches before code execution.
Unlike classes that generate runtime JavaScript code, interfaces exist purely at the type level. They disappear during compilation, leaving zero footprint in the generated JavaScript while providing complete type safety during development. This makes them ideal for describing data structures, API contracts, and component props without runtime overhead.
TypeScript's structural type system means any object matching an interface's shape satisfies that interface, regardless of explicit declarations. This "duck typing" approach differs from nominal typing in languages like Java, enabling flexible composition patterns while maintaining type safety.
In this guide, you'll learn:
- How interfaces define object shapes and enforce type contracts
- The difference between interfaces and type aliases
- Extending interfaces to build complex type hierarchies
- Using interfaces for function signatures and class contracts
Prerequisites
To follow this guide, you'll need Node.js 18+:
Setting up the project
Create and configure a new TypeScript project with ES module support:
Initialize with ES modules:
Install dependencies:
Next, create a TypeScript configuration file:
This initializes a tsconfig.json file, establishing a modern TypeScript environment. With these steps complete, you have everything needed to explore interfaces with immediate code execution through tsx.
Understanding the shape mismatch problem
JavaScript functions frequently receive objects as parameters, expecting specific properties to exist with certain types. Without type checking, functions blindly access properties that might be missing, misspelled, or contain wrong value types—bugs that only surface at runtime when the code executes.
These shape mismatches cause "Cannot read property of undefined" errors, unexpected type coercion, and silent failures where wrong data propagates through your application. The calling code assumes one object structure while the function expects another, and JavaScript provides no mechanism to catch this mismatch before execution.
Let's examine a typical scenario where object shape violations cause runtime failures:
Run this code to see the problems:
The function silently handles missing and misspelled properties by outputting undefined. JavaScript executes without complaint, but the data corruption manifests later when other code relies on these properties having valid values.
Check whether TypeScript catches these problems:
TypeScript compiles successfully without errors because the function parameter has implicit any type. The compiler treats the parameter as "anything goes," providing no protection against shape mismatches.
Solving shape problems with interfaces
Interfaces define the exact shape an object must have, specifying required properties and their types. When you declare a parameter as an interface type, TypeScript verifies that passed objects contain all required properties with correct types, catching shape violations during compilation.
This transforms implicit assumptions about object structure into explicit contracts. The interface becomes documentation that's enforced by the compiler, preventing objects with wrong shapes from reaching your functions.
Let's fix the previous example with an interface:
Now check what TypeScript reports:
TypeScript now catches both shape violations at compile time. The first error identifies the missing age property with a precise message showing exactly what's missing. The second error detects the typo and even suggests the correct property name. These errors appear in your editor as you type, preventing shape mismatches from ever reaching production.
How interface type checking works
TypeScript uses structural typing to verify interface conformance. When you pass an object to a function expecting an interface, the compiler checks that the object has all required properties with compatible types. The object can have additional properties—TypeScript only cares that it has at least what the interface requires.
This differs from nominal typing where explicit declarations matter. In TypeScript, you don't need to declare that an object implements an interface. If the object has the right shape, it satisfies the interface automatically:
The point object never mentions the Point interface, but TypeScript accepts it because the structure matches. This structural approach enables flexible composition while maintaining type safety through compile-time shape verification.
Extending interfaces for composition
Interfaces support extension through the extends keyword, enabling composition of complex types from simpler building blocks. An interface can extend one or multiple interfaces, inheriting all their properties while adding new ones. This creates type hierarchies that mirror domain relationships without code duplication.
Extension works additively—the derived interface requires all properties from base interfaces plus any newly declared properties. This makes it impossible to violate base interface contracts while allowing specialization.
Let's build a user system with interface extension:
Run this to see interface hierarchy in action:
The Manager interface extends Employee, which extends Person, creating a three-level hierarchy. A Manager object satisfies all three interfaces because it contains all required properties from the chain. This enables polymorphic function calls—sendEmail accepts any Person, so it works with Employee and Manager objects too.
Multiple interface extension enables mixing behaviors:
The User interface combines properties from three separate interfaces, creating a composite type without manually redefining shared properties.
Using interfaces for function signatures
Interfaces describe more than object shapes—they can define function signatures, creating contracts for callbacks, event handlers, and higher-order functions. This enables type-safe function parameters where the structure of the function itself is part of the type contract.
Function signature interfaces specify parameter types and return types without implementation. Any function matching this signature satisfies the interface, enabling polymorphic function passing with full type safety.
Let's build a data processing pipeline with function interfaces:
Run the function interface example:
The Transformer and Validator interfaces define function shapes, while Processor combines them into an object interface. This creates composable processing logic where any function matching the signature works, regardless of implementation details.
Function interfaces enable powerful abstraction patterns in event systems and middleware:
This pattern appears throughout TypeScript's standard library and enables type-safe callback registration without sacrificing flexibility.
Final thoughts
Interfaces transform implicit assumptions about object shapes into explicit compile-time contracts, catching structure mismatches during development rather than production. They scale from simple property definitions to complex type hierarchies through extension and composition.
TypeScript's structural type system makes interfaces flexible: any object with the right shape satisfies an interface regardless of explicit declarations. This enables duck typing with compile-time guarantees while still preserving static type safety.
Interfaces operate entirely at compile time, generating zero runtime code while providing complete type checking during development. This makes them ideal for performance-critical applications where type safety matters but runtime overhead is unacceptable.
Explore the TypeScript handbook to learn more about advanced interface patterns. You’ll see how interfaces integrate with classes, generics, and utility types to create robust type systems.