Introducción: ¿por qué se busca esto con tanta frecuencia?
\n\nCada desarrollador que migra de JavaScript a TypeScript o simplemente escribe en este lenguaje, tarde o temprano se enfrenta al problema: ¿cómo establecer correctamente un valor predeterminado para un objeto que se pasa a una función? En Stack Overflow, esta pregunta ha acumulado miles de visitas y decenas de respuestas. ¿Por qué? Porque, aunque parece una tarea simple — "si no se pasa un objeto, usa un objeto vacío" — en TypeScript se convierte en toda una odisea con tipos, interfaces y desestructuración.
\n\nEn este artículo, analizaremos todas las formas posibles, desde las más simples hasta las más avanzadas, con ejemplos de código y explicaciones. Aprenderá cómo configurar valores predeterminados para un objeto TypeScript pasado como argumento y evitar errores típicos. ¡Allá vamos!
\n\nEl problema: ¿qué tiene de malo el enfoque habitual?
\n\nSupongamos que tenemos una función que recibe un objeto con configuraciones:
\n\nfunction createUser(config: { name?: string; age?: number }) {\n // ...\n}\n\ncreateUser(); // ¡Error! Se necesita un argumento\n\n\nEn JavaScript, simplemente podríamos escribir function createUser(config = {}) y listo. Pero en TypeScript, este truco no funciona: el compilador se quejará por la falta de coincidencia de tipos. ¿Por qué? Porque el objeto vacío {} no coincide con la interfaz { name?: string; age?: number } — simplemente no tiene esas propiedades.
Y aquí es donde comienza lo interesante. Analicemos todas las formas de resolverlo.
\n\nForma 1: Desestructuración con asignación de valores predeterminados
\n\nEl método más elegante y popular es usar la desestructuración directamente en los parámetros de la función. Extrae las propiedades del objeto y les asigna valores predeterminados de inmediato:
\n\nfunction createUser({ name = "Invitado", age = 18 }: { name?: string; age?: number } = {}) {\n console.log(`Usuario creado: ${name}, edad: ${age}`);\n}\n\ncreateUser(); // "Usuario creado: Invitado, edad: 18"\ncreateUser({ name: "Ana" }); // "Usuario creado: Ana, edad: 18"\ncreateUser({ age: 25 }); // "Usuario creado: Invitado, edad: 25"\n\n\nObserve el = {} al final. Este es el valor predeterminado para el objeto en sí. Si no pasa un argumento, TypeScript usa un objeto vacío y la desestructuración asigna los valores predeterminados para cada propiedad.
Ventajas: limpio, conciso, funciona como un reloj.
\nDesventajas: es necesario especificar explícitamente el tipo para cada propiedad.
Forma 2: Uso de una interfaz con Partial
\n\nSi su objeto es grande, describir todos los tipos en los parámetros es de mala educación. Saque la interfaz por separado y use Partial<T>:
interface UserConfig {\n name: string;\n age: number;\n email: string;\n isAdmin: boolean;\n}\n\nfunction createUser(config: Partial<UserConfig> = {}) {\n const defaults: UserConfig = {\n name: "Invitado",\n age: 18,\n email: "noemail@example.com",\n isAdmin: false\n };\n \n const mergedConfig = { ...defaults, ...config };\n console.log(mergedConfig);\n}\n\ncreateUser({ name: "Iván" });\n// { name: "Iván", age: 18, email: "noemail@example.com", isAdmin: false }\n\n\nAquí, Partial<UserConfig> hace que todas las propiedades sean opcionales. Dentro de la función, fusionamos los valores predeterminados con los pasados. Funciona perfectamente, pero requiere una fusión manual.
Forma 3: Enfoque clásico con verificación
\n\nA veces se necesita más control. Por ejemplo, si desea establecer un valor predeterminado solo para undefined, pero no para null. Usamos el clásico if:
function createUser(config?: UserConfig) {\n const name = config?.name ?? "Invitado";\n const age = config?.age ?? 18;\n const email = config?.email ?? "noemail@example.com";\n \n console.log({ name, age, email });\n}\n\ncreateUser();