Argument Destructuring and Type Annotations in TypeScript
I often use destructuring in ES6 when I want to have a function with an options
object. I described options
objects in ”More useful function patterns - function overloading as a way to achieve function overloading in JavaScript with the added benefits of named arguments and extensibility.
Recently I was trying to use the same pattern in TypeScript adding some type annotations but it did not work! If you have had the same issue yourself read through to find out how to solve it.
Options Objects
Here’s an example of an options object from the aforementioned article to give you an idea of what I am talking about:
// use an object to wrap parameters:
// - it gives you named arguments
// - and painless extension if more arguments are required
function raiseSkeletonWithOptions(spellOptions) {
spellOptions = spellOptions || {}
var armySize = spellOptions.armySize || 1,
creatureType = spellOptions.creatureType || ''
if (creatureType) {
console.log('raise a skeleton ' + creatureType)
} else {
console.log('raise ' + armySize + ' skeletons ' + creatureType)
}
}
raiseSkeletonWithOptions()
// => raise a skeleton
raiseSkeletonWithOptions({ armySize: 4 })
// => raise 4 skeletons
raiseSkeletonWithOptions({ creatureType: 'king' })
// => raise skeleton king
This is ES5 but it can be rewritten using ES6 destructuring and defaults (you can check this article if you want to learn more about ES6 Destructuring by the by):
// use an object to wrap parameters:
// - it gives you named arguments
// - and painless extension if more arguments are required
function raiseSkeletonWithOptions({ armySize = 1, creatureType = '' } = {}) {
if (creatureType) {
console.log('raise a skeleton ' + creatureType)
} else {
console.log('raise ' + armySize + ' skeletons ' + creatureType)
}
}
raiseSkeletonWithOptions()
// => raise a skeleton
raiseSkeletonWithOptions({ armySize: 4 })
// => raise 4 skeletons
raiseSkeletonWithOptions({ creatureType: 'king' })
// => raise skeleton king
Arguments Destructuring in TypeScript
So I was trying to follow this pattern in TypeScript and I started by writing the following ES6 code:
function say({ something = 'hello world 1' } = { something: 'hello world 2' }) {
console.log(something)
}
Which you can evaluate providing different arguments:
// No arguments => the default object kicks in
// {something:'hello world 2'}
say()
// => hello world 2
// An empty object as argument
// The default values kick in
// {something='hello world 1'}
say({})
// => hello world 1
// An object with a something property
// the something property provided is used
say({ something: 'muhahaha' })
// => muahaha
The next step was to add type annotations to this previous example. So I went and added them:
function say(
{ something: string = 'hello world 1' } = { something: 'hello world 2' }
) {
console.log(something) // something not defined
}
But when I tried to call that function everything exploded!
say()
// => Uncaught ReferenceError: something is not defined
What? What? What? I asked myself… isn’t TypeScript supposed to be a superset of ES6?
Then I realized that type annotations syntax conflicts with ES6 destructuring syntax. For instance, you can use the :
with destructuring to extract and project a value to a different variable than in the original object:
const options = { something: 'hello world' }
const { something: helloWorld } = options
console.log(helloWorld)
// => hello world
console.log(something)
// => something is not defined
So it makes sense that TypeScript doesn’t attempt to redefine the meaning of :
in this particular case where it has an specific meaning in ES6. Otherwise it wouldn’t be a superset of ES6 but a different language.
So, is there a way we can still get type annotations in this scenario? Yes it is. And you can thank Anders Hejlsberg for his super quick answer. You can add top level type annotations like this:
function say(
{ something = 'hello world 1' }: { something: string } = {
something: 'hello world 2',
}
) {
console.log(something)
}
Which works just as you would expect and gives you type annotations with type safety and nice intellisense:
say()
// => hello world 2
say({})
// => hello world 1
say({ something: 'muhahaha' })
// => muahaha
It is a little bit wordy though so you may consider splitting it like this:
function say(data: { something?: string } = { something: 'hello world 2' }) {
const { something = 'hello world 1' } = data
console.log(something)
}
Or:
const defaultOptions = { something: 'hello world 2' }
function say(data: { something?: string } = defaultOptions) {
const { something = 'hello world 1' } = data
console.log(something)
}
And that’s it! Now you can use destructuring, defaults and type annotations.
Have a nice Friday and an even better weekend! :)
Written by Jaime González García , dad, husband, software engineer, ux designer, amateur pixel artist, tinkerer and master of the arcane arts. You can also find him on Twitter jabbering about random stuff.