barbarian meets coding

WebDev, UX & a Pinch of Fantasy

Argument Destructuring and Type Annotations in TypeScript

| Comments

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.

A Screenshot of me testing destructuring in TypeScript

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//  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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//  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:

1
2
3
function say({something='hello world 1'}={something:'hello world 2'}){
  console.log(something);
}

Which you can evaluate providing different arguments:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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:

1
2
3
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!

1
2
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:

1
2
3
4
5
6
7
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:

1
2
3
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:

1
2
3
4
5
6
7
8
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:

1
2
3
4
function say(data:{something?:string}={something:"hello world 2"}){
  const {something="hello world 1"} = data;
  console.log(something);
}

Or:

1
2
3
4
5
6
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! :)

Comments