mergeOptions
Merge two optional objects with type-safe undefined handling
Usage
Merges two option objects into a new object with superior type inference compared to Object.assign. The key advantage is proper handling of undefined values and optional properties.
import * as _ from 'radashi'
const defaults = { port: 3000, host: 'localhost' }const userConfig = { port: 8080, ssl: true }
_.mergeOptions(defaults, userConfig)// => { port: 8080, host: 'localhost', ssl: true }Handling Undefined Arguments
When either argument is undefined, the function returns the other argument with proper typing. When both are undefined, the result is correctly typed as undefined.
import * as _ from 'radashi'
// First argument undefined → returns second_.mergeOptions(undefined, { b: 2 })// => { b: 2 }
// Second argument undefined → returns first_.mergeOptions({ a: 1 }, undefined)// => { a: 1 }
// Both undefined → returns undefined_.mergeOptions(undefined, undefined)// => undefinedType Safety with Undefined
The main advantage of mergeOptions over Object.assign is how it handles optional objects (T | undefined).
With Object.assign, you often have to use workarounds that result in complex intersection types.
import * as _ from 'radashi'
type Config = { x: number; y?: string } | undefinedtype Override = { y: string; z: boolean } | undefined
declare const config: Configdeclare const override: Override
// ❌ Using Object.assign with a type assertion forces `config` to be non-nullable,// but `Config` originally allows `undefined`. This assertion changes the intent,// and results in a complex intersection typeconst bad1 = Object.assign(config as NonNullable<Config>, override)// ^? { x: number; y?: string } & { y: string; z: boolean }// Potential runtime error if `config` is undefined.
// ❌ Using Object.assign with an empty object as a workaround is hackyconst bad2 = Object.assign({}, config, override)// ^? { x: number; y?: string } & { y: string; z: boolean }// Still produces a complex intersection type
// ✅ mergeOptions models every runtime branchconst good = _.mergeOptions(config, override)// ^?// | undefined// | { x: number; y?: string }// | { y: string; z: boolean }// | { x: number; y: string; z: boolean }// No runtime error if `config` is undefined.The type of good clearly shows all possible runtime states, making it easier to understand and work with.
Real-World Example
This pattern is common when merging user configuration with defaults:
import * as _ from 'radashi'import type { MergeOptions } from 'radashi'
type UserConfig = { theme?: 'light' | 'dark'; lang?: string } | undefinedtype AppDefaults = { theme: 'light'; lang: 'en' }
function getConfig( userPreferences: UserConfig, defaults: AppDefaults,): MergeOptions<AppDefaults, UserConfig> { return _.mergeOptions(defaults, userPreferences) // The return type covers both defaults-only and merged config branches.}
// Runtime results:getConfig(undefined, { theme: 'light', lang: 'en' })// => { theme: 'light', lang: 'en' }
getConfig({ theme: 'dark' }, { theme: 'light', lang: 'en' })// => { theme: 'dark', lang: 'en' }With Class Instances
Works seamlessly with class instances, merging their properties into a plain object.
import * as _ from 'radashi'
class Character { constructor( public name: string, public age: number, ) {}}
const anderson = new Character('Thomas A. Anderson', 30)const neo = { name: 'Neo', alias: 'The One' }
_.mergeOptions(anderson, neo)// => { name: 'Neo', age: 30, alias: 'The One' }