JavaScript
Validación de datos
Explica cómo parsear, normalizar y validar datos externos por tipo, forma y reglas del dominio antes de convertirlos en valores confiables.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
- Fundamentos de programación
JavaScript
Explica cómo parsear, normalizar y validar datos externos por tipo, forma y reglas del dominio antes de convertirlos en valores confiables.
Validar significa comprobar que un valor cumple las condiciones necesarias antes de utilizarlo como dato confiable.
function parsePrice(value) {
const price = Number(value);
if (!Number.isFinite(price)) {
throw new TypeError(
"Price must be a finite number",
);
}
if (price < 0) {
throw new RangeError(
"Price cannot be negative",
);
}
return price;
}Información externa:
Aunque una API prometa cierta forma, el programa debe manejar respuestas incompletas, versiones distintas y errores de integración.
const data = JSON.parse(content);JSON.parse comprueba sintaxis JSON. No garantiza que data tenga las propiedades del dominio.
Son pasos relacionados, pero distintos.
Convertir una representación a un valor manipulable.
const data = JSON.parse(content);Llevar variantes válidas a una representación consistente.
const email = input.trim().toLowerCase();Comprobar reglas.
if (!email.includes("@")) {
throw new ValidationError("Invalid email");
}Una secuencia común:
entrada externa
↓
parsear
↓
normalizar
↓
validar
↓
valor confiableNo normalices de forma que esconda información inválida. Por ejemplo, convertir cualquier texto imposible a 0 puede crear un precio aparentemente válido.
function assertString(value, field) {
if (typeof value !== "string") {
throw new TypeError(
`${field} must be a string`,
);
}
}Para arrays:
if (!Array.isArray(value)) {
throw new TypeError(
"items must be an array",
);
}Para números:
if (!Number.isFinite(value)) {
throw new TypeError(
"value must be a finite number",
);
}typeof value === "number" también acepta NaN e Infinity, que muchas reglas no desean.
Antes de acceder a propiedades, comprueba que existe un objeto apropiado.
function isRecord(value) {
return (
typeof value === "object" &&
value !== null &&
!Array.isArray(value)
);
}function parseUser(value) {
if (!isRecord(value)) {
throw new TypeError(
"User must be an object",
);
}
if (!Object.hasOwn(value, "name")) {
throw new ValidationError(
"User name is required",
);
}
assertString(value.name, "name");
return {
name: value.name.trim(),
};
}Object.hasOwn diferencia una propiedad propia de una propiedad heredada.
Peligroso:
function parseUser({ name }) {
return name.trim();
}Si el argumento es null o undefined, falla antes de que la función pueda producir un error comprensible.
Más controlado:
function parseUser(value) {
if (!isRecord(value)) {
throw new TypeError(
"User must be an object",
);
}
const { name } = value;
// Validar name.
}Un dato puede tener el tipo correcto y seguir siendo inválido.
const stock = 10;Es un número válido, pero quizá la regla exige un entero no negativo.
if (!Number.isInteger(stock)) {
throw new TypeError(
"Stock must be an integer",
);
}
if (stock < 0) {
throw new RangeError(
"Stock cannot be negative",
);
}Las reglas de dominio incluyen:
function parsePeriod(value) {
const start = new Date(value.start);
const end = new Date(value.end);
if (
Number.isNaN(start.getTime()) ||
Number.isNaN(end.getTime())
) {
throw new ValidationError(
"Dates are invalid",
);
}
if (end < start) {
throw new ValidationError(
"End date cannot be before start date",
);
}
return { start, end };
}Cada fecha puede ser válida individualmente, pero la relación puede no tener sentido.
Estos estados no son iguales:
undefined → no se proporcionó
null → ausencia explícita
"" → string presente pero vacíoDefine el contrato.
function parseDescription(value) {
if (value === undefined) {
return null;
}
if (typeof value !== "string") {
throw new TypeError(
"Description must be a string",
);
}
const description = value.trim();
return description === "" ? null : description;
}Aquí vacío y ausente se normalizan intencionalmente a null.
Un default debe cubrir ausencia válida, no cualquier valor falsy.
const quantity = input.quantity ?? 1;Con ||, un valor legítimo como 0 también sería reemplazado.
Después del default, valida.
if (!Number.isInteger(quantity)) {
throw new TypeError(
"Quantity must be an integer",
);
}function parseTags(value) {
if (!Array.isArray(value)) {
throw new TypeError(
"Tags must be an array",
);
}
const tags = value.map((tag, index) => {
if (typeof tag !== "string") {
throw new ValidationError(
`Tag at index ${index} must be a string`,
);
}
return tag.trim();
});
return [...new Set(tags)].filter(Boolean);
}Se valida cada elemento, se normaliza y se elimina duplicación.
Detenerse en el primer problema:
if (name === "") {
throw new ValidationError(
"Name is required",
);
}Es útil cuando:
Para formularios suele ser mejor mostrar todos los campos inválidos.
function validateRegistration(input) {
const issues = [];
if (!input.email?.includes("@")) {
issues.push({
field: "email",
code: "INVALID_EMAIL",
});
}
if (
!Number.isInteger(input.age) ||
input.age < 18
) {
issues.push({
field: "age",
code: "MINIMUM_AGE",
});
}
return issues;
}El consumidor decide si convertir la lista en un ValidationError.
Resultado explícito:
function safeParsePrice(value) {
const price = Number(value);
if (!Number.isFinite(price)) {
return {
success: false,
error: {
code: "INVALID_NUMBER",
},
};
}
return {
success: true,
data: price,
};
}Lanzar:
function parsePrice(value) {
// Lanza cuando no puede producir un precio.
}Un resultado discriminado es útil cuando el fallo es esperado y el consumidor debe tomar una decisión inmediata. Lanzar es útil cuando una función no puede cumplir su contrato y la propagación simplifica las capas intermedias.
Mantén consistencia en APIs relacionadas.
Validar pregunta si el valor cumple reglas.
Sanitizar transforma o elimina partes potencialmente peligrosas para un contexto.
const title = input.trim();Esto normaliza espacios; no vuelve seguro un string para insertarlo como HTML.
La seguridad depende del destino:
Utiliza APIs seguras y escaping contextual. No confíes en una función genérica de “limpieza” para todos los destinos.
El frontend valida para mejorar experiencia.
El backend valida para proteger el sistema.
Un cliente puede saltarse la interfaz, modificar una solicitud o usar otra versión de la aplicación. La validación del servidor sigue siendo necesaria.
Herramientas como Zod, Valibot, JSON Schema u otras pueden:
No sustituyen entender:
function calculateTotal(validatedItems) {
// Núcleo que confía en su entrada.
}Si cada función vuelve a validar el mismo objeto, el código se llena de ruido y puede aplicar reglas inconsistentes.
Valida en fronteras y crea valores confiables para el núcleo. Vuelve a validar cuando el dato cruza otra frontera o puede haber sido modificado externamente.
function parseProduct(value) {
if (!isRecord(value)) {
throw new TypeError(
"Product must be an object",
);
}
if (typeof value.id !== "string") {
throw new ValidationError(
"Product id is required",
);
}
if (typeof value.name !== "string") {
throw new ValidationError(
"Product name is required",
);
}
const price = Number(value.price);
if (!Number.isFinite(price) || price < 0) {
throw new ValidationError(
"Product price is invalid",
);
}
return {
id: value.id,
name: value.name.trim(),
price,
};
}La función devuelve una nueva forma controlada en lugar de permitir que el objeto externo viaje por toda la aplicación.
||.¿Por qué esto no valida realmente una respuesta?
const product = JSON.parse(responseText);
return product.name.toUpperCase();JSON.parse solo comprueba que el texto tenga sintaxis JSON válida. product podría ser null, un array o un objeto sin name. Debe validarse su forma antes de utilizarlo.
Manejo y propagación de errores explica qué capa debe recuperar, traducir, registrar o permitir que un fallo continúe.