Definición y sintaxis de interfaces
Después de explorar las bases de las clases en TypeScript, es momento de introducir las interfaces. Las interfaces son una herramienta poderosa que nos permite definir la forma que deben tener los objetos, actuando como contratos que garantizan que ciertas estructuras de datos cumplan con un conjunto específico de propiedades y métodos.
En esta lección, aprenderemos a definir interfaces y cómo nuestras clases pueden implementarlas para asegurar que cumplen con el contrato definido por la interfaz.
¿Qué es una interfaz?
Una interfaz en TypeScript es como un plano o un contrato. Define qué propiedades y métodos (y sus tipos) debe tener un objeto para ser considerado compatible con esa interfaz. A diferencia de las clases, las interfaces no tienen implementación (no tienen lógica dentro de los métodos, solo la firma) y no existen en el código JavaScript final después de la compilación; son una característica puramente de TypeScript para la verificación de tipos en tiempo de desarrollo.
Se declaran utilizando la palabra clave interface
.
// Definición básica de una interfaz para representar una persona
interface Person {
// Propiedades requeridas (obligatorias)
id: number;
name: string;
// Propiedad opcional (puede estar presente o no)
age?: number; // El signo ? indica que esta propiedad es opcional
// Propiedad de solo lectura (solo se puede asignar en la inicialización)
readonly passportNumber: string;
// Métodos que un objeto que cumpla esta interfaz debe tener (solo la firma)
greet(message: string): void; // Un método llamado greet que recibe un string y no devuelve nada
getDetails(): string; // Un método llamado getDetails que devuelve un string
}
- Propiedades: Defines el nombre y tipo de las propiedades. Pueden ser obligatorias (por defecto), opcionales (
?
), o de solo lectura (readonly
). La palabra clavereadonly
en una interfaz significa que un objeto que cumpla esa interfaz no debería poder modificar esa propiedad una vez que se le asigna un valor (generalmente en el momento de su creación). - Métodos: Defines solo la firma del método: su nombre, los tipos de sus parámetros y su tipo de retorno. No se incluye el cuerpo del método (la lógica).
Las interfaces son increíblemente flexibles y pueden describir la forma de casi cualquier objeto.
Implementando interfaces en clases
El propósito principal de definir interfaces es crear contratos que nuestras clases puedan prometer cumplir. Cuando una clase implementa
una interfaz, TypeScript verifica que esa clase proporcione todos los miembros requeridos (propiedades y métodos obligatorios) definidos en la interfaz, con los tipos correctos.
Para hacer que una clase implemente una interfaz, usamos la palabra clave implements
seguida del nombre de la interfaz. Una clase puede implementar una o varias interfaces (separadas por comas).
Sintaxis básica
La sintaxis para implementar una interfaz es la siguiente:
// Definimos una interfaz para un servicio de almacenamiento
interface StorageService {
// Método para guardar datos
save(key: string, value: string): void;
// Método para cargar datos
load(key: string): string | null; // Puede devolver un string o null
}
// Creamos una clase que implementa StorageService
// Esta clase DEBE tener los métodos save y load con las firmas exactas
class LocalStorageService implements StorageService {
// Las propiedades requeridas por la interfaz irían aquí si las hubiera (ej: un 'name')
// name: string = "local-storage"; // Ejemplo si la interfaz tuviera 'name: string;'
// Implementación del método save de la interfaz
save(key: string, value: string): void {
localStorage.setItem(key, value); // Lógica real del método
console.log(`Datos guardados para la clave: ${key}`);
}
// Implementación del método load de la interfaz
load(key: string): string | null {
const data = localStorage.getItem(key); // Lógica real del método
console.log(`Datos cargados para la clave: ${key}`);
return data; // Devuelve string o null, según la firma de la interfaz
}
// Podemos añadir métodos adicionales que NO estén en la interfaz
clearAll(): void {
localStorage.clear();
console.log("Limpiado todo el almacenamiento local.");
}
}
// Creamos una instancia de la clase que implementa la interfaz
const myStorage = new LocalStorageService();
myStorage.save("username", "alice"); // Usar un método implementado
const user = myStorage.load("username"); // Usar otro método implementado
if (user) {
console.log(`Usuario cargado: ${user}`);
}
myStorage.clearAll(); // Usar un método adicional de la clase
TypeScript verificará en tiempo de compilación si LocalStorageService
realmente tiene save
y load
con las firmas correctas. Si falta alguno o la firma no coincide (por ejemplo, save
devuelve number
), TypeScript mostrará un error.
Implementando Propiedades Opcionales y Readonly
Cuando una interfaz tiene propiedades opcionales (?
) o de solo lectura (readonly
), la clase que la implementa debe cumplir con esos requisitos:
- Propiedades Opcionales: La clase puede definir la propiedad opcional, pero no está obligada a hacerlo.
- Propiedades
readonly
: La clase debe definir la propiedadreadonly
y asegurarse de que solo se le asigne un valor (generalmente en el constructor).
interface Configuration {
// Propiedad obligatoria
apiUrl: string;
// Propiedad opcional
timeout?: number;
// Propiedad de solo lectura
readonly version: string;
}
class ApiService implements Configuration {
apiUrl: string; // Implementa propiedad obligatoria
timeout?: number; // Implementa propiedad opcional (podríamos omitirla)
readonly version: string; // Implementa propiedad readonly
constructor(url: string, apiVersion: string, requestTimeout?: number) {
this.apiUrl = url;
this.version = apiVersion; // Asignamos el valor a la propiedad readonly aquí
this.timeout = requestTimeout; // Asignamos valor a la opcional si existe
}
// Método para ilustrar uso (no requerido por la interfaz)
fetchData(): void {
console.log(`Workspaceing from ${this.apiUrl} (v${this.version}) with timeout ${this.timeout || 'default'}`);
// No podríamos hacer: this.version = "new-version"; // Error porque version es readonly
}
}
const service = new ApiService("https://api.example.com", "1.0", 5000);
service.fetchData();
const simpleService = new ApiService("https://api.another.com", "2.0"); // Omitimos el timeout opcional
simpleService.fetchData();
Implementando múltiples interfaces
Una clase puede implementar múltiples interfaces simultáneamente, lo que permite componer comportamientos de diferentes contratos:
// Interfaz para algo que se puede guardar
interface Saveable {
save(): boolean; // Devuelve true si se guardó correctamente
}
// Interfaz para algo que se puede cargar
interface Loadable {
load(id: string): boolean; // Carga por ID, devuelve true si se encontró
}
// Nuestra clase que implementa ambas interfaces
class UserProfileManager implements Saveable, Loadable {
userId: string | null = null;
userData: any = null; // Usamos 'any' temporalmente, idealmente sería un tipo más específico
constructor(initialUserId?: string) {
if (initialUserId) {
this.load(initialUserId); // Intentar cargar al iniciar
}
}
// Implementación del método save de Saveable
save(): boolean {
if (this.userId && this.userData) {
console.log(`Guardando datos para usuario ${this.userId}...`);
// Aquí iría la lógica real para guardar (ej: a un archivo, API)
return true; // Simulación de guardado exitoso
}
console.log("No hay datos o ID de usuario para guardar.");
return false;
}
// Implementación del método load de Loadable
load(id: string): boolean {
console.log(`Cargando datos para usuario ${id}...`);
// Aquí iría la lógica real para cargar (ej: desde un archivo, API)
// Simulación: encontrar datos si el ID es "user123"
if (id === "user123") {
this.userId = id;
this.userData = { name: "Charlie", age: 40 }; // Datos cargados
console.log("Usuario cargado exitosamente.");
return true; // Simulación de carga exitosa
}
console.log("Usuario no encontrado.");
this.userId = null;
this.userData = null;
return false; // Simulación de carga fallida
}
}
// Creamos una instancia del Manager
const manager = new UserProfileManager();
manager.load("user123"); // Llama al método load
manager.save(); // Llama al método save
manager.load("user456"); // Llama al método load (fallará)
manager.save(); // Llama al método save (fallará)
Implementación con propiedades privadas
Las interfaces solo definen la estructura pública de una clase. Podemos implementar una interfaz y añadir propiedades o métodos privados adicionales:
interface ICounter {
count: number;
increment(): void;
decrement(): void;
reset(): void;
}
class Counter implements ICounter {
count: number;
private initialValue: number;
constructor(initialValue: number = 0) {
this.count = initialValue;
this.initialValue = initialValue;
}
increment(): void {
this.count++;
}
decrement(): void {
this.count--;
}
reset(): void {
this.count = this.initialValue;
}
// Método privado adicional que no está en la interfaz
private logCount(): void {
console.log(`Current count: ${this.count}`);
}
}
Implementación con accesores
Podemos implementar propiedades de interfaz utilizando getters y setters en la clase:
interface IPerson {
firstName: string;
lastName: string;
fullName: string;
}
class Person implements IPerson {
private _firstName: string;
private _lastName: string;
constructor(firstName: string, lastName: string) {
this._firstName = firstName;
this._lastName = lastName;
}
get firstName(): string {
return this._firstName;
}
set firstName(value: string) {
this._firstName = value;
}
get lastName(): string {
return this._lastName;
}
set lastName(value: string) {
this._lastName = value;
}
get fullName(): string {
return `${this._firstName} ${this._lastName}`;
}
}
const person = new Person("John", "Doe");
console.log(person.fullName); // Output: John Doe
person.firstName = "Jane";
console.log(person.fullName); // Output: Jane Doe
Caso práctico: implementación de interfaces para componentes
Las interfaces son especialmente útiles cuando desarrollamos componentes reutilizables:
interface IComponent {
render(): string;
update(props: any): void;
}
class Button implements IComponent {
private label: string;
private onClick: () => void;
constructor(label: string, onClick: () => void) {
this.label = label;
this.onClick = onClick;
}
render(): string {
return `<button class="btn" onclick="handleClick()">${this.label}</button>`;
}
update(props: { label?: string; onClick?: () => void }): void {
if (props.label) {
this.label = props.label;
}
if (props.onClick) {
this.onClick = props.onClick;
}
}
// Método específico de Button que no está en la interfaz
handleClick(): void {
this.onClick();
}
}
class Image implements IComponent {
private src: string;
private alt: string;
constructor(src: string, alt: string) {
this.src = src;
this.alt = alt;
}
render(): string {
return `<img src="${this.src}" alt="${this.alt}" />`;
}
update(props: { src?: string; alt?: string }): void {
if (props.src) {
this.src = props.src;
}
if (props.alt) {
this.alt = props.alt;
}
}
}
La implementación de interfaces en clases es una práctica fundamental en TypeScript que promueve el diseño orientado a contratos y facilita la creación de código más modular, mantenible y fácil de probar.
Extensión de interfaces
Las interfaces en TypeScript no solo pueden ser implementadas por clases, sino que también pueden extenderse entre sí para crear estructuras más complejas y reutilizables. La extensión de interfaces nos permite construir interfaces más específicas a partir de otras más generales, siguiendo el principio de composición que es fundamental en el diseño de software.
Sintaxis básica de extensión
Para extender una interfaz, utilizamos la palabra clave extends
:
interface Animal {
name: string;
age: number;
}
interface Pet extends Animal {
owner: string;
vaccinated: boolean;
}
En este ejemplo, la interfaz Pet
hereda todas las propiedades de Animal
(name
y age
) y añade sus propias propiedades (owner
y vaccinated
). Cualquier objeto que implemente Pet
deberá proporcionar valores para las cuatro propiedades.
const myDog: Pet = {
name: "Rex",
age: 3,
owner: "Alice",
vaccinated: true
};
Extendiendo múltiples interfaces
Una interfaz puede extender de múltiples interfaces a la vez, combinando todos sus miembros.
// Interfaz para identificar algo
interface Identifiable {
id: string;
}
// Interfaz para algo que tiene una fecha de creación
interface Timestamped {
createdAt: Date;
}
// Interfaz para un Usuario (hereda de Identifiable y Timestamped)
// Un User DEBE tener id (de Identifiable) y createdAt (de Timestamped)
interface UserProfileInterface extends Identifiable, Timestamped {
name: string; // Además, un UserProfile tiene un nombre
}
// Una clase que implemente UserProfileInterface debe tener id, createdAt y name
class BasicUser implements UserProfileInterface {
id: string; // De Identifiable
createdAt: Date; // De Timestamped
name: string; // De UserProfileInterface
constructor(id: string, name: string) {
this.id = id;
this.name = name;
this.createdAt = new Date(); // Inicializamos el miembro heredado
}
}
const basicUser = new BasicUser("user-007", "James Bond");
console.log(`${basicUser.name} (ID: ${basicUser.id}) created at ${basicUser.createdAt.toDateString()}`);
Esta técnica es especialmente útil para crear interfaces modulares que pueden combinarse según las necesidades específicas de cada caso.
Redefinición de propiedades
Al extender interfaces, podemos redefinir propiedades para hacerlas más específicas, siempre que el nuevo tipo sea compatible con el original:
interface Shape {
color: string;
area(): number;
}
interface Circle extends Shape {
radius: number;
// Redefinimos el método area con una firma más específica
area(): number;
}
class CircleImpl implements Circle {
color: string;
radius: number;
constructor(color: string, radius: number) {
this.color = color;
this.radius = radius;
}
area(): number {
return Math.PI * this.radius * this.radius;
}
}
Extensión y sobrescritura de propiedades opcionales
Podemos convertir propiedades opcionales en obligatorias al extender interfaces:
interface Config {
endpoint: string;
timeout?: number;
retries?: number;
}
interface StrictConfig extends Config {
// Convertimos propiedades opcionales en obligatorias
timeout: number;
retries: number;
// Añadimos nuevas propiedades
logLevel: 'debug' | 'info' | 'warn' | 'error';
}
const devConfig: Config = {
endpoint: "https://api.dev.example.com"
// timeout y retries son opcionales
};
const prodConfig: StrictConfig = {
endpoint: "https://api.example.com",
timeout: 5000, // Ahora es obligatorio
retries: 3, // Ahora es obligatorio
logLevel: "error"
};
Interfaces anidadas y extensión
Las interfaces pueden contener definiciones de otras interfaces, y estas también pueden extenderse:
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
interface Person {
name: string;
address: Address;
}
interface Employee extends Person {
employeeId: string;
department: string;
// Podemos extender la definición anidada
address: Address & {
officeNumber?: string;
};
}
const employee: Employee = {
name: "John Smith",
employeeId: "E12345",
department: "Engineering",
address: {
street: "123 Main St",
city: "San Francisco",
zipCode: "94105",
country: "USA",
officeNumber: "4B"
}
};
Interfaces vs. tipos: cuándo usar cada uno
Ya conoces los alias de tipo (type
) para dar nombres a tipos, y ahora las interfaces (interface
) para definir la forma de los objetos y clases. A menudo, pueden parecer muy similares, especialmente para definir la estructura de un objeto:
Sintaxis básica
La sintaxis para definir ambos constructos es ligeramente diferente:
// Usando Type Alias para la forma de un punto
type PointType = {
x: number;
y: number;
};
// Usando Interface para la forma de un punto
interface PointInterface {
x: number;
y: number;
}
// Ambos funcionan para tipar variables y objetos literales
const point1: PointType = { x: 10, y: 20 };
const point2: PointInterface = { x: 30, y: 40 };
// Y son estructuralmente compatibles (puedes asignar uno al otro si tienen la misma forma)
const point3: PointType = point2; // OK
const point4: PointInterface = point1; // OK
Sin embargo, hay diferencias clave en lo que pueden describir y cómo se comportan en cuanto a extensibilidad y uso con clases:
- Lo que pueden describir:
interface
: Principalmente describe la forma de objetos. Puede tener propiedades y firmas de métodos.type
: Puede dar nombre a cualquier tipo, incluyendo primitivos (type UserId = string;
), uniones (type Status = "active" | "inactive";
), tuplas (type Coordinates = [number, number];
), firmas de funciones (type Comparator = (a, b) => number;
), y la forma de objetos.
- Extensibilidad y Clases:
interface
: Puede ser extendida por otras interfaces (interface Pet extends Animal {}
) y implementada por clases (class Dog implements Pet {}
). Están diseñadas para la herencia y la implementación de contratos en OOP.type
: Puede dar nombre a la forma de un objeto, pero no puede ser implementado directamente por una clase usandoimplements
en el mismo sentido. No tienen el mecanismoextends
nativo para crear herencia de formas de objeto (aunque puedes combinar tipos usando intersecciones&
, que es un concepto diferente).
Guía práctica: ¿Cuándo usar cada uno?
Basándonos en las diferencias anteriores, aquí hay una guía práctica para decidir cuándo usar interfaces y cuándo usar tipos:
Usa interfaces cuando:
- Defines clases que implementarán la estructura:
interface Repository<T> {
findAll(): Promise<T[]>;
findById(id: string): Promise<T | null>;
save(item: T): Promise<T>;
}
class UserRepository implements Repository<User> {
// Implementación...
}
- Creas bibliotecas o APIs públicas que otros desarrolladores pueden necesitar extender:
// En tu biblioteca
interface Theme {
primaryColor: string;
secondaryColor: string;
}
// Los consumidores pueden extender
interface CustomTheme extends Theme {
tertiaryColor: string;
fontFamily: string;
}
- Necesitas fusionar declaraciones del mismo nombre:
// Definición base
interface Config {
apiUrl: string;
}
// En otro archivo o módulo
interface Config {
timeout: number;
}
// Uso combinado
const config: Config = {
apiUrl: "https://api.example.com",
timeout: 3000
};
Usa tipos cuando:
- Necesitas uniones o intersecciones:
type Result<T> = SuccessResult<T> | ErrorResult;
type UserWithRoles = User & {
roles: string[];
permissions: string[];
};
- Trabajas con tuplas o arrays específicos:
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Point2D = [number, number];
type RGB = [number, number, number];
type RGBA = [...RGB, number];
- Necesitas tipos utilitarios o transformaciones:
type Nullable<T> = T | null;
type KeysOf<T> = keyof T;
type UserKeys = KeysOf<User>; // "id" | "name" | "email"
- Defines alias para tipos primitivos o funciones:
type UserId = string;
type ValidationFunction<T> = (value: T) => boolean;
type EventHandler = (event: Event) => void;
Consideraciones de compatibilidad
Es importante destacar que interfaces y tipos son estructuralmente compatibles entre sí si sus estructuras coinciden:
interface IPoint {
x: number;
y: number;
}
type TPoint = {
x: number;
y: number;
};
// Esto es válido - son estructuralmente equivalentes
const p1: IPoint = { x: 10, y: 20 };
const p2: TPoint = p1;
Recomendación general
Una regla práctica que muchos equipos de desarrollo siguen es:
- Usa
interface
principalmente cuando estés definiendo la forma de un objeto o el contrato que una clase va a cumplir. Son ideales para la programación orientada a objetos y la definición de estructuras que se implementarán. - Usa
type
cuando necesites dar nombre a cualquier otro tipo (primitivos, uniones simples, tuplas, firmas de funciones) o cuando quieras definir la forma de un objeto de manera más flexible (aunque para formas de objeto simples, la elección entreinterface
ytype
es a menudo una cuestión de preferencia o convención del equipo).
Ambas herramientas son esenciales en TypeScript y las verás combinadas a menudo en proyectos reales. La clave es entender sus propósitos principales y sus diferencias para usarlas de forma efectiva.

Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de TypeScript
Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Definir contratos de objeto con
interface
. - Implementar interfaces en clases con
implements
. - Implementar múltiples interfaces en una clase.
- Extender interfaces para reutilizar contratos.
- Comprender la diferencia básica entre
interface
ytype
.