Una propiedad opcional de TypeScript parece sencilla: escribes timeoutMs?: number y sigues. El problema aparece cuando un objeto se construye con spreads o cuando una API interpreta "clave ausente" como "usa el valor predeterminado". En ese caso, {} y { timeoutMs: undefined } no son el mismo objeto.
Con exactOptionalPropertyTypes: true, el compilador hace explícita esa diferencia. timeoutMs?: number permite omitir la clave o enviar un número. Si el contrato también acepta una clave presente con undefined, escribe timeoutMs?: number | undefined. La opción ayuda cuando la presencia de la clave tiene significado, pero no sustituye la validación en runtime.
Este artículo es un spoke de la validación de tool calls en TypeScript. Ese owner cubre los datos no confiables que llegan a una tool. Esta página responde la pregunta anterior: ¿qué objeto puede construir el código y qué debe significar cada estado?

Respuesta breve
- Usa
prop?: Tcuando la clave pueda omitirse y, cuando esté presente, deba contenerT.- Usa
prop?: T | undefinedcuando una clave presente conundefinedsea un estado válido.- Activa
exactOptionalPropertyTypescuando el código distinga ausencia,undefinedy quizánull.- Revisa los objetos construidos con spread: pasar
undefinedpara decir "no informado" puede dejar una clave presente.- Sigue validando JSON, respuestas HTTP y argumentos de tools en runtime.
¿Qué cambia con exactOptionalPropertyTypes?
Sin esta opción, TypeScript trata una propiedad opcional como si su tipo de asignación también aceptara undefined en muchas situaciones. Esto normalmente compila:
interface UserPreferences {
colorTheme?: "dark" | "light";
}
const preferences: UserPreferences = {
colorTheme: undefined,
};
Con "exactOptionalPropertyTypes": true, la declaración se lee de forma más literal. colorTheme?: "dark" | "light" significa que la clave puede estar ausente o contener una de las dos cadenas. Asignar undefined se convierte en un error, a menos que undefined aparezca explícitamente en el tipo.
La referencia del TSConfig sobre exactOptionalPropertyTypes usa el mismo contraste: la ausencia representa un estado, mientras que una clave con undefined sigue siendo una clave. Las notas de TypeScript 4.4 también explican que la opción no forma parte de la familia strict y requiere strictNullChecks.
Esto cambia sobre todo lo que el código puede escribir. Cuando lees preferences.colorTheme, el valor todavía puede ser "dark" | "light" | undefined porque la clave puede faltar. La opción no convierte una lectura opcional en una lectura obligatoria.
¿Por qué una clave ausente es diferente de undefined?
JavaScript puede representar los dos estados:
const omitted = {};
const present = { timeoutMs: undefined };
console.log("timeoutMs" in omitted); // false
console.log("timeoutMs" in present); // true
console.log(Object.keys(omitted)); // []
console.log(Object.keys(present)); // ["timeoutMs"]
El operador in comprueba si una propiedad existe en un objeto o en su cadena de prototipos. MDN también señala que una propiedad puede existir mientras su valor sea undefined, así que "x" in object no equivale a object.x !== undefined.
La diferencia aparece al combinar opciones. Imagina un executor que mantiene un valor predeterminado cuando no se proporcionó timeoutMs:
type ExecutorOptions = {
timeoutMs?: number;
};
const defaults = { timeoutMs: 10_000 };
const incoming: ExecutorOptions = { timeoutMs: undefined };
const options = { ...defaults, ...incoming };
console.log(options.timeoutMs); // undefined
El spread no sabe que undefined quería decir "no cambies el valor predeterminado". Copia la propiedad presente. Object.assign() sigue la misma idea: copia propiedades propias enumerables y las fuentes posteriores sobrescriben las claves anteriores (MDN, Object.assign()).
La FAQ de TypeScript usa un ejemplo parecido para mostrar por qué los valores undefined de Partial<T> pueden ser sospechosos al combinarlos con spread (FAQ de TypeScript). Eso no hace incorrecto todo spread. El tipo aún tiene que indicar si importa la presencia de la clave.
¿Cómo se modelan las configuraciones y opciones?
Empieza por escribir el contrato antes de activar la opción. Hay al menos tres intenciones diferentes.
La clave puede omitirse, pero no puede ser undefined
Usa una propiedad opcional normal cuando el llamador pueda dejar el valor fuera:
type ToolOptions = {
timeoutMs?: number;
signal?: AbortSignal;
};
const defaults: ToolOptions = {};
const withTimeout: ToolOptions = { timeoutMs: 5_000 };
Con exactOptionalPropertyTypes, const invalid: ToolOptions = { timeoutMs: undefined } falla. El error es útil cuando el executor distingue una opción que no se proporcionó de otra que se configuró explícitamente.
Una clave presente con undefined es válida
Algunas APIs usan la presencia de la clave para estandarizar opciones genéricas, componer adapters o conservar una firma pública. En ese caso, declara el estado de forma explícita:
type PublicOptions = {
timeoutMs?: number | undefined;
};
const allowed: PublicOptions = { timeoutMs: undefined };
El ? sigue permitiendo omitir la clave. | undefined añade la posibilidad de que la clave exista con ese valor. No añadas la unión solo para silenciar el compilador. Documenta una decisión del contrato.
null representa otra operación
En un payload de actualización, la ausencia puede significar "no cambiar" y null puede significar "limpiar":
type ProfilePatch = {
displayName?: string;
bio?: string | null;
};
const rename: ProfilePatch = { displayName: "Ada" };
const clearBio: ProfilePatch = { bio: null };
Ese significado pertenece a tu API. exactOptionalPropertyTypes ayuda a mantener fuera el undefined accidental, pero no decide si null debe limpiar un campo. Documenta la regla y prueba los estados aceptados en el límite que recibe el payload.
Para un registro de tools escrito en el código, usa satisfies para comprobar la configuración sin perder la inferencia. Son comprobaciones complementarias: satisfies comprueba la forma estática del objeto, mientras exactOptionalPropertyTypes impide tratar una clave presente con undefined como si estuviera ausente.
¿Cómo activar la opción sin avanzar a ciegas?
Actívala junto con strictNullChecks, como en este tsconfig.json mínimo:
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"exactOptionalPropertyTypes": true,
"noEmit": true
}
}
El comando de verificación puede ser sencillo:
npx tsc --noEmit
Para proteger un comportamiento concreto, usa una prueba de tipos con @ts-expect-error:
type RequestOptions = {
cache?: "no-store" | "force-cache";
};
const request: RequestOptions = {};
request.cache = "no-store";
// @ts-expect-error: el contrato exige ausencia o un modo conocido
request.cache = undefined;
Una migración segura separa cuatro preguntas:
- ¿El error apareció porque el código quería omitir la clave? Construye el objeto de forma condicional.
- ¿Apareció porque
undefinedes un estado público válido? Añade| undefineda la declaración que expresa esa intención. - ¿Vino de una dependencia o de tipos generados? Comprueba la versión y el tipo que originó la incompatibilidad. No lo ocultes con una aserción global.
- ¿Vino de un valor externo? No lo soluciones cambiando una interfaz. Valida los datos antes de convertirlos en un tipo interno.
Para la primera pregunta, evita insertar la clave cuando la condición sea falsa:
const options: RequestOptions = {
...(useCache ? { cache: "force-cache" } : {}),
};
La guía sobre cómo validar JSON externo antes de usarlo en TypeScript cubre el límite diferente: un tipo estático no confirma que el JSON recibido tenga la forma declarada.
¿Qué no demuestra esta opción?
exactOptionalPropertyTypes actúa durante la comprobación de asignaciones de TypeScript. No inspecciona una respuesta HTTP, no valida un mensaje de cola ni impide que any o una aserción incorrecta atraviesen el programa. Tampoco garantiza que la implementación respete el significado documentado de null.
En un tool call, por ejemplo, todavía tienes que validar los argumentos recibidos antes de ejecutar. La validación de tool calls en TypeScript cubre ese contrato de runtime. El compilador puede impedir que tu código construya un objeto inválido; no puede comprobar por sí solo un objeto creado fuera del proceso.
También existe un coste de compatibilidad. La opción afecta a todas las propiedades opcionales del proyecto, no solo a una interfaz. Los tipos públicos, el código generado y las dependencias pueden asumir que prop?: T acepta undefined. La FAQ de TypeScript recomienda entender ese impacto en el contexto del proyecto. No existe una configuración por tipo que active la semántica solo para una interfaz aislada.
Checklist de decisión
Antes de activar la opción, responde:
- ¿Una clave ausente significa "usar el valor predeterminado", "heredar" o "no cambiar"?
- ¿Una clave presente con
undefinedtiene otro comportamiento? - ¿
nulles el valor para limpiar o debe rechazarse? - ¿Algún spread u
Object.assign()copia opciones que contienenundefined? - ¿Las declaraciones públicas y los tipos generados aceptan la semántica que quieres?
- ¿El código sigue validando la entrada externa en runtime?
- ¿Existe una prueba de compilación que falle si la diferencia vuelve a desaparecer?
Si la respuesta a la primera pregunta es sí y undefined no tiene un significado separado, exactOptionalPropertyTypes es una forma directa de hacer que el tipo refleje el contrato. Si no, dejar la opción desactivada puede ser coherente, siempre que esa flexibilidad sea intencional y esté cubierta por pruebas.
Método y límites
Samuel Fajreldines es responsable de este artículo. La investigación comparó la referencia actual del TSConfig, las notas de TypeScript 4.4, la FAQ del proyecto y la documentación de MDN sobre la presencia de propiedades y Object.assign(). La fixture mínima se compiló con una instalación temporal de TypeScript y el ejemplo de presencia se ejecutó con Node.js. Los ejemplos demuestran el contrato descrito, pero no son un benchmark de migración ni una medición de compatibilidad del ecosistema.
Fuentes
- TypeScript,
exactOptionalPropertyTypes, consultado el 9 de noviembre de 2026. - TypeScript, notas de la versión 4.4, consultadas el 9 de noviembre de 2026.
- TypeScript, FAQ: Require Properties to Either Be Missing or Not
undefined, consultada el 9 de noviembre de 2026. - MDN, operador
in, consultado el 9 de noviembre de 2026. - MDN,
Object.assign(), consultado el 9 de noviembre de 2026.