Una interfaz pone nombre a la forma de un objeto: las propiedades que debe tener y el tipo de cada una. Una vez declarada, usas el nombre como tipo, y el compilador comprueba contra ella cada objeto que pasas, devuelves o asignas.
La última llamada es el error de compilación TS2741. Las interfaces se borran al compilar el código: la salida en JavaScript no tiene ni rastro de User, y nada comprueba la forma en tiempo de ejecución.
Declarar una interfaz
La sintaxis es la palabra clave interface, un nombre (en PascalCase por convención) y un cuerpo con los miembros. Los miembros se pueden separar con punto y coma, con comas o simplemente con saltos de línea; el punto y coma es el estilo habitual.
interface Product {
sku: string; // required property
price: number;
tags: string[]; // array property
dimensions: { // nested object type
width: number;
height: number;
};
discount?: number; // optional property
readonly createdAt: Date; // cannot be reassigned
label(): string; // method
}
Una interfaz es un tipo, no un valor. No se puede instanciar con new, no tiene valores por defecto, y obj instanceof Product es el error TS2693 ('Product' only refers to a type, but is being used as a value here). Para comprobar una forma en tiempo de ejecución, escribe un type guard.
Tipado estructural y comprobación de propiedades de más
TypeScript compara formas, no nombres. Cualquier objeto con las propiedades requeridas encaja en la interfaz, se haya declarado con ella o no. Las propiedades de más están permitidas, con una excepción: un objeto literal escrito directamente donde se espera la interfaz pasa por una comprobación de propiedades de más, porque una clave desconocida ahí casi siempre es una errata.
Ese error (TS2353) es el que detecta { id: 1, name: "a", emial: "x" } para un User: el compilador incluso sugiere Did you mean to write 'email'? (TS2561).
Propiedades opcionales y readonly
Un ? después del nombre hace opcional una propiedad: el objeto puede omitirla, y leerla da T | undefined. readonly prohíbe reasignar la propiedad después de crear el objeto.
Aquí se ven dos límites. Primero, readonly es solo de tiempo de compilación: las dos líneas marcadas con @ts-expect-error se ejecutan igualmente al pulsar Run, y funcionan, y las últimas líneas cambian apiUrl a través de una referencia tipada sin readonly. Segundo, es superficial: readonly hosts: string[] impediría reasignar hosts pero seguiría permitiendo hosts.push(...), y por eso el propio array está tipado como readonly string[]. Documenta y hace cumplir la intención en el código tipado; no congela nada. El utility type Readonly<T> hace readonly de una vez todas las propiedades de una interfaz existente.
Métodos y propiedades de tipo función
Un método se puede escribir como firma de método, name(params): ReturnType, o como una propiedad que contiene una función, name: (params) => ReturnType. Quien los usa los llama igual.
La diferencia es sutil: con strictFunctionTypes (parte de strict), los parámetros de las propiedades de tipo función se comprueban de forma estricta, mientras que las firmas de método se comprueban de forma más laxa (bivariante), así que la forma de propiedad detecta algunos errores más. La sintaxis de método es más corta y el estilo más habitual; las dos están bien.
Una interfaz también puede describir algo que se puede llamar o construir, con una firma de llamada o una firma de construcción:
interface Formatter {
(value: number): string; // call signature: the object is a function
locale: string; // and it also has a property
}
interface PointConstructor {
new (x: number, y: number): { x: number; y: number }; // construct signature
}
Firmas de índice
Cuando los nombres de las propiedades no se conocen de antemano, una firma de índice las describe todas a la vez: [key: string]: T significa «cualquier clave string, cada una con un T».
Las últimas líneas muestran la pega: leer una clave que no existe tiene tipo number, no number | undefined. La opción del compilador noUncheckedIndexedAccess añade | undefined a cada lectura de ese tipo.
Las propiedades con nombre pueden convivir con una firma de índice, pero tienen que encajar en ella. interface Dict { [key: string]: number; name: string } es el error TS2411, Property 'name' of type 'string' is not assignable to 'string' index type 'number'. Amplía el tipo del índice ([key: string]: number | string) o lleva la parte dinámica a su propia propiedad. Para mapas simples de clave y valor, Record<string, number> dice lo mismo en una línea.
Extender una interfaz
extends construye una interfaz nueva a partir de una o varias existentes. La hija tiene todos los miembros del padre más los suyos:
interface Animal {
name: string;
}
interface Pet extends Animal {
owner: string;
}
interface Trained {
commands: string[];
}
interface ServiceDog extends Pet, Trained {
certifiedUntil: Date;
}
// ServiceDog requires: name, owner, commands, certifiedUntil
Una hija solo puede volver a declarar una propiedad del padre con un tipo compatible (más estrecho), como kind: "dog" donde el padre dice kind: string. Las reglas, y cómo extender alias de tipo, están en la página de extends.
Implementar una interfaz en una clase
class X implements Shape le pide al compilador que compruebe que la clase tiene todo lo que exige la interfaz. Un miembro que falta es un error en la declaración de la clase:
index.ts(7,7): error TS2420: Class 'Circle' incorrectly implements interface 'Shape'.
Property 'area' is missing in type 'Circle' but required in type 'Shape'.
Con area() añadido, varias clases e incluso un objeto normal se pueden usar como Shape:
implements es solo una comprobación. No añade miembros a la clase, y no tipa por ti los parámetros de sus métodos: greet(name) {} dentro de una clase que implementa greet(name: string): string sigue siendo el error TS7006, Parameter 'name' implicitly has an 'any' type. Una clase puede implementar varias interfaces: class A implements B, C.
Fusión de declaraciones
Declarar dos veces una interfaz con el mismo nombre en el mismo ámbito fusiona las dos en una. Es algo que los alias de tipo no pueden hacer (un segundo type con el mismo nombre es un error de identificador duplicado).
interface Settings {
theme: string;
}
interface Settings {
fontSize: number;
}
// Settings now requires both properties
const s: Settings = { theme: "dark", fontSize: 14 };
En código de aplicación esto rara vez es lo que quieres, y una fusión accidental puede confundir. Su uso real es añadir miembros a tipos que no son tuyos: las opciones de una librería, o una global como Window. Desde dentro de un módulo, envuelve la declaración en declare global:
declare global {
interface Window {
analytics: { track(event: string): void };
}
}
export {};
Después de esto, window.analytics.track("signup") pasa la comprobación de tipos en todo el proyecto. Los paquetes de definiciones de tipos se basan en el mismo mecanismo; consulta archivos de declaración.
Valores por defecto en las propiedades de una interfaz
Una interfaz no puede contener valores por defecto, porque describe tipos y se borra en tiempo de ejecución. size?: "sm" | "md" = "md" es el error TS1246, An interface property cannot have an initializer. Haz opcional la propiedad y rellena el valor por defecto donde se usa el objeto:
Los valores por defecto en la desestructuración son la opción más segura: se aplican siempre que el valor es undefined, incluido un size: undefined explícito. La versión con expansión copia ese undefined explícito encima del valor por defecto, y su resultado sigue tipado como si size estuviera siempre asignado. Si necesitas esa garantía en los tipos, activa exactOptionalPropertyTypes, que convierte size: undefined en un error de compilación para un size?: ... opcional. Una clase con campos inicializados es la otra opción cuando el objeto también necesita comportamiento.
Interfaces genéricas
Una interfaz puede recibir parámetros de tipo, lo que hace que una sola declaración funcione con muchos tipos de contenido:
interface ApiResponse<T> {
ok: boolean;
data: T;
error?: string;
}
interface Page<T> {
items: T[];
nextCursor?: string;
}
interface User {
id: number;
name: string;
}
const res: ApiResponse<Page<User>> = {
ok: true,
data: { items: [{ id: 1, name: "Ada" }], nextCursor: "abc" },
};
ApiResponse<Page<User>> se lee como «una respuesta cuyos datos son una página de usuarios». La librería estándar está llena de ellas: Array<T>, Promise<T> y Map<K, V> son todas interfaces genéricas.
Interfaz frente a alias de tipo
Un alias type puede describir la misma forma de objeto, y para tipos objeto simples los dos son intercambiables. Solo una interfaz se puede fusionar; solo un alias de tipo puede nombrar una unión, una tupla, o un mapped type o conditional type. La regla práctica del handbook de TypeScript es usar interface hasta que necesites algo que solo tiene type. La página de interface frente a type tiene la comparación completa, incluida la diferencia con Record<string, ...> que sorprende a casi todo el mundo.
Preguntas frecuentes
¿Qué es una interfaz en TypeScript?
Una interfaz es una descripción con nombre de la forma de un objeto: los nombres de sus propiedades, sus tipos, cuáles son opcionales o readonly, y sus métodos. El compilador comprueba que los valores usados como esa interfaz tienen esa forma. Las interfaces solo existen en tiempo de compilación; no producen JavaScript.
¿Cómo pongo un valor por defecto en una interfaz de TypeScript?
No se puede: una interfaz describe tipos, no valores, así que size: "md" = ... no es sintaxis válida. Marca la propiedad como opcional (size?: "sm" | "md") y aplica el valor por defecto donde se usa el objeto, normalmente con valores por defecto en la desestructuración de los parámetros de la función: function render({ size = "md" }: Options). Expandir un objeto de valores por defecto ({ ...DEFAULTS, ...options }) también funciona, pero un undefined explícito en options sobrescribe el valor por defecto.
¿Cómo compruebo en tiempo de ejecución si un objeto implementa una interfaz?
No hay una forma integrada, porque las interfaces se borran al compilar: obj instanceof User es el error TS2693 ('User' only refers to a type, but is being used as a value here). Escribe una función type guard que compruebe las propiedades, function isUser(x: unknown): x is User { ... }, o valida con una librería de esquemas.
¿Puede una interfaz extender varias interfaces?
Sí. Enuméralas después de extends, separadas por comas: interface ServiceDog extends Pet, Trained { ... }. La nueva interfaz tiene todos los miembros de cada padre más los suyos. Si dos padres declaran la misma propiedad con tipos incompatibles, la declaración es un error.
¿Qué diferencia hay entre una interfaz y una clase en TypeScript?
Una clase existe en tiempo de ejecución: tiene un constructor, implementaciones de métodos, y new crea objetos a partir de ella. Una interfaz solo describe una forma para el compilador y se borra de la salida en JavaScript. Una clase puede declarar implements SomeInterface para que el compilador compruebe que encaja, y cualquier objeto normal con la forma correcta también encaja en la interfaz.