Typing Destructured Object Parameters in TypeScript
In TypeScript, you can add a type annotation to each formal parameter of a function using a colon and the desired type, like this:
function greet(name: string) {
return `Hello ${name}!`;
}
That way, your code doesn't compile when you attempt to call the function with an argument of an incompatible type, such as number
or boolean
. Easy enough.
Let's now look at a function declaration that makes use of destructuring assignment with an object parameter, a feature that was introduced as part of ECMAScript 2015. The toJSON
function accepts a value
of any type that should be stringified as JSON. It additionally accepts a settings parameter that allows the caller to provide configuration options via properties:
function toJSON(value: any, { pretty }) {
const indent = pretty ? 4 : 0;
return JSON.stringify(value, null, indent);
}
The type of the value
parameter is explicitly given as any
, but what type does the pretty
property have? We haven't explicitly specified a type, so it's implicitly typed as any
. Of course, we want it to be a boolean, so let's add a type annotation:
function toJSON(value: any, { pretty: boolean }) {
const indent = pretty ? 4 : 0;
return JSON.stringify(value, null, indent);
}
However, that doesn't work. The TypeScript compiler complains that it can't find the name pretty
that is used within the function body. This is because boolean
is not a type annotation in this case, but the name of the local variable that the value of the pretty
property gets assigned to. Again, this is part of the specification of how object destructuring works.
Because TypeScript is a superset of JavaScript, every valid JavaScript file is a valid TypeScript file (set aside type errors, that is). Therefore, TypeScript can't simply change the meaning of the destructuring expression { pretty: boolean }
. It looks like a type annotation, but it's not.
#Typing Immediately Destructured Parameters
Of course, TypeScript offers a way to provide an explicit type annotation. It's a little verbose, yet (if you think about it) consistent:
function toJSON(value: any, { pretty }: { pretty: boolean }) {
const indent = pretty ? 4 : 0;
return JSON.stringify(value, null, indent);
}
You're not directly typing the pretty
property, but the settings object it belongs to, which is the actual parameter passed to the toJSON
function. If you now try to compile the above TypeScript code, the compiler doesn't complain anymore and emits the following JavaScript function:
function toJSON(value, _a) {
var pretty = _a.pretty;
var indent = pretty ? 4 : 0;
return JSON.stringify(value, null, indent);
}
#Providing Default Values
To call the above toJSON
function, both the value
and the settings parameter have to be passed. However, it might be reasonable to use default settings if they aren't explicitly specified. Assuming that pretty
should be true
by default, we'd like to be able to call the function in the following various ways:
const value = { foo: "bar" };
toJSON(value, { pretty: true }); // #1
toJSON(value, {}); // #2
toJSON(value); // #3
The function call #1 already works because all parameters are specified. In order to enable function call #2, we have to mark the pretty
property as optional by appending a question mark to the property name within the type annotation. Additionally, the pretty
property gets a default value of true
if it's not specified by the caller:
function toJSON(value: any, { pretty = true }: { pretty?: boolean }) {
const indent = pretty ? 4 : 0;
return JSON.stringify(value, null, indent);
}
Finally, function call #3 is made possible by providing a default value of {}
for the destructuring pattern of the settings object. If no settings object is passed at all, the empty object literal {}
is being destructured. Because it doesn't specify a value for the pretty
property, its fallback value true
is returned:
function toJSON(value: any, { pretty = true }: { pretty?: boolean } = {}) {
const indent = pretty ? 4 : 0;
return JSON.stringify(value, null, indent);
}
Here's what the TypeScript compiler emits when targeting "ES5"
:
function toJSON(value, _a) {
var _b = (_a === void 0 ? {} : _a).pretty,
pretty = _b === void 0 ? true : _b;
var indent = pretty ? 4 : 0;
return JSON.stringify(value, null, indent);
}
When targeting "ES6"
, only the type information is removed:
function toJSON(value, { pretty = true } = {}) {
const indent = pretty ? 4 : 0;
return JSON.stringify(value, null, indent);
}
#Extracting a Type for the Settings Parameter
With multiple properties, the inline type annotation gets unwieldy quickly, which is why it might a good idea to create an interface for the configuration object:
interface SerializerSettings {
pretty?: boolean;
}
You can now type the settings parameter using the new interface type:
function toJSON(value: any, { pretty = true }: SerializerSettings = {}) {
const indent = pretty ? 4 : 0;
return JSON.stringify(value, null, indent);
}