Default object parameters in TypeScript

2022-07-12

In vanilla JavaScript, a reasonable way to handle a function that might otherwise have many parameters is to instead give it a single object parameter, with the original parameters as properties. This is especially nice when the properties have sensible defaults.

function foo({a = 1, b = 2, c = 3} = {}) {
  return { a, b, c };
}

// we can call it like this
foo({a: 7});         // => { a: 7, b: 2, c: 3 }
foo({b: 0, c: 0 });  // => { a: 1, b: 0, c: 0 }
foo();               // => { a: 1, b: 2, c: 3 }

Each property in the object has a default, and if no object at all is passed, an empty object is used instead, which has each of its properties replaced by the default ones. Cool, right?

What are we to do in TypeScript? We probably have a type for the thing going into the function:

type Thing = {
  a: number;
  b: number;
  c: number;
};

function foo({a, b, c }: Thing): Thing {
  return { a, b, c };
}

But then how do we handle default arguments? Any missing properties will mean our object is not a Thing. We could make all the properties optional on Thing, but it feels kinda gross. If a thing is not a valid Thing without all the properties, we don’t want any of those properties to be optional. A few months ago, I did not know how to solve this neatly, so I spent a while futzing with options.

The moral of this story is that Partial exists. A Partial<Thing> is a Thing where every property is optional. Because Partial is a type, we can inline it in the annotation without modifying our Thing type definition at all. We know our function will be dealing with a valid Thing, because we provide a default Thing.

type Thing = {
  a: number;
  b: number;
  c: number;
};

function foo({ a = 1, b = 2, c = 3 }: Partial<Thing> = {}): Thing {
  return { a, b, c };
}

// and the calls work the same
foo({ a: 7 });        // => { a: 7, b: 2, c: 3 }
foo({ b: 0, c: 0 });  // => { a: 1, b: 0, c: 0 }
foo();                // => { a: 1, b: 2, c: 3 }

This feels like the perfect use for Partial. It is nice that TypeScript is incrementally adoptable, and starts providing benefits as soon as a few judicious number and string annotations are added. But it’s really nice that the more I lean into the type system, the better it gets.


Thanks for reading. If you'd like words like these in your inbox, sign up below. Or just check this site occasionally. Or run, now, and never speak of this place. You do you.