TypeScript
Objetos y tipado estructural
Cómo TypeScript tipa objetos mediante su estructura, comprueba compatibilidad, trata propiedades opcionales y aplica excess property checks en literales.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
TypeScript
Cómo TypeScript tipa objetos mediante su estructura, comprueba compatibilidad, trata propiedades opcionales y aplica excess property checks en literales.
TypeScript describe objetos por su forma: qué propiedades existen, qué tipos contienen y qué operaciones ofrecen.
type Product = {
id: string;
price: number;
};function printProduct(product: {
id: string;
price: number;
}) {
console.log(product.id, product.price);
}Es útil para contratos pequeños usados una sola vez.
Cuando la forma representa un concepto reutilizable, un alias o una interface mejora el vocabulario:
type Product = {
id: string;
price: number;
};type Product = {
id: string;
price: number;
};
const product: Product = {
id: "product-1",
price: 100,
};Si falta una propiedad:
const product: Product = {
id: "product-1",
};TypeScript detecta que price es obligatoria.
type Product = {
id: string;
description?: string;
};Al leer:
product.description;
// string | undefinedUna propiedad opcional no significa automáticamente que puedas utilizarla como string.
type Product = {
readonly id: string;
price: number;
};
product.id = "another-id";La asignación produce un error estático.
readonly no congela el objeto:
Object.isFrozen(product); // falseTampoco es profundo:
type Order = {
readonly customer: {
name: string;
};
};
order.customer.name = "Ana";La referencia customer no puede reemplazarse, pero el objeto anidado continúa mutable.
type Product = {
id: string;
calculatePrice(tax: number): number;
};Implementación:
const product: Product = {
id: "product-1",
calculatePrice(tax) {
return 100 * (1 + tax);
},
};Los parámetros y el retorno reciben contextual typing.
type Product = {
calculatePrice: (tax: number) => number;
};Ambas formas expresan una operación callable, pero existen diferencias de compatibilidad de parámetros en contextos avanzados. Para callbacks almacenadas como datos, la propiedad función suele hacer visible esa intención.
type Named = {
name: string;
};
function greet(value: Named) {
return `Hola, ${value.name}`;
}
const developer = {
name: "Nicolás",
role: "Frontend Developer",
};
greet(developer);El objeto contiene la propiedad requerida, por lo que es compatible.
No importa:
type UserId = string;
type ProductId = string;
const userId: UserId = "123";
const productId: ProductId = userId;Ambos aliases tienen la misma estructura: string.
Los nombres mejoran documentación, pero no crean identidades incompatibles. Para eso se necesitan marcas o wrappers.
type ProductPreview = {
id: string;
name: string;
};
const fullProduct = {
id: "product-1",
name: "Keyboard",
price: 100,
stock: 4,
};
const preview: ProductPreview = fullProduct;El valor ofrece al menos lo requerido.
Un literal escrito directamente recibe una comprobación adicional:
type ProductPreview = {
id: string;
name: string;
};
const preview: ProductPreview = {
id: "product-1",
name: "Keyboard",
price: 100,
};price produce un error porque no pertenece al contrato esperado en ese literal.
const value = {
id: "product-1",
name: "Keyboard",
price: 100,
};
const preview: ProductPreview = value;Es compatible estructuralmente.
Esto demuestra que TypeScript no implementa objetos exactos por defecto. La excess property check ayuda a detectar typos y errores en literales frescos, pero no prohíbe cualquier propiedad adicional en todos los valores.
const preview: ProductPreview = {
id: "product-1",
name: "Keyboard",
pirce: 100,
};La comprobación adicional detecta pirce, que probablemente era price.
type Route = {
path: `/${string}`;
public: boolean;
};
const routes = {
home: {
path: "/",
public: true,
},
dashboard: {
path: "/dashboard",
public: false,
},
} satisfies Record<string, Route>;Comprueba la estructura sin reemplazar las claves específicas home y dashboard por un string genérico.
type Address = {
city: string;
postalCode: string;
};
type Customer = {
id: string;
address: Address;
};Separar Address aporta cuando:
No existe una regla de “nunca anidar tipos”. Una forma pequeña que solo tiene sentido dentro de otra puede permanecer inline.
type Customer = {
id: string;
preferences: {
receiveEmails: boolean;
};
};type Scores = {
[playerId: string]: number;
};Indica que cualquier clave string válida produce un number.
const scores: Scores = {
playerA: 10,
playerB: 20,
};type Inventory = {
total: number;
[productId: string]: number;
};Todas las propiedades string, incluida total, deben ser compatibles con number.
No puedes declarar:
type Inventory = {
total: number;
status: string;
[productId: string]: number;
};porque status también cae bajo el index signature string.
Más claro:
type Inventory = {
total: number;
quantities: Record<string, number>;
};Las propiedades conocidas no quedan forzadas al mismo tipo que las claves dinámicas.
type Scores = Record<string, number>;
const score = scores[playerId];Sin el flag, score puede verse como number aunque la clave no exista.
Con noUncheckedIndexedAccess:
number | undefinedEl tipo refleja la posibilidad runtime.
const identifier = "productId";
const product = {
[identifier]: "product-1",
};TypeScript puede conservar la clave literal cuando el binding es suficientemente específico.
Si la clave es string genérico, la forma resultante puede requerir un index signature.
function readProperty(
product: Product,
key: string,
) {
return product[key];
}No cualquier string es una propiedad de Product.
Contrato correcto:
function readProperty(
product: Product,
key: keyof Product,
) {
return product[key];
}keyof y los indexed access types se estudiarán después porque el retorno todavía depende de la clave concreta.
type RequestState =
| {
status: "loading";
}
| {
status: "success";
data: Product[];
}
| {
status: "error";
error: Error;
};Cada objeto tiene una forma relacionada con status. Este patrón evita objetos con muchas propiedades opcionales incompatibles entre sí.
Se desarrollará dentro de discriminated unions.
Menos seguro:
type RequestState = {
loading: boolean;
data?: Product[];
error?: Error;
};Permite combinaciones ambiguas:
{
loading: true,
data: products,
error,
}Modelar variantes separadas expresa mejor el dominio.
type Audited = {
createdAt: Date;
updatedAt: Date;
};
type Product = {
id: string;
name: string;
} & Audited;El resultado necesita propiedades de ambos tipos.
Intersections tienen matices cuando dos propiedades usan tipos incompatibles y se estudiarán con composición de tipos.
const product = {
id: "product-1",
price: 100,
};
const updated = {
...product,
price: 120,
};TypeScript infiere una nueva forma. El spread sigue las reglas runtime de JavaScript y es superficial.
El checker no garantiza que una operación externa no haya añadido propiedades o prototipos inesperados.
const keys = Object.keys(product);El tipo suele ser string[], no (keyof Product)[].
En runtime un objeto puede contener claves adicionales a las que conoce su tipo, por lo que TypeScript evita prometer exactitud universal.
No hagas assertion a keyof automáticamente sin controlar el origen del objeto.
const value: object = await response.json();Esto solo afirma que no es primitivo. No demuestra propiedades.
Mejor:
const value: unknown = await response.json();
const product = parseProduct(value);type ProductResponse = {
id: string;
price_in_cents: number;
};
type Product = {
id: string;
priceInCents: number;
};
function toProduct(
response: ProductResponse,
): Product {
return {
id: response.id,
priceInCents: response.price_in_cents,
};
}Separar formas evita que el contrato externo invada todo el código.
readonly es estático y superficial.noUncheckedIndexedAccess modela claves inexistentes.¿Por qué este código puede ser válido aunque value tenga una propiedad adicional?
const value = {
id: "product-1",
name: "Keyboard",
price: 100,
};
const preview: {
id: string;
name: string;
} = value;Porque TypeScript utiliza compatibilidad estructural: value contiene al menos las propiedades requeridas. La comprobación de propiedades adicionales más estricta se aplica principalmente a literales escritos directamente en el lugar esperado.
Funciones y firmas de llamada inicia el siguiente bloque y aplica estos contratos a parámetros, retornos y callbacks.