Destructuring assignment and rest parameters are awesome and typical in codebases these days. Is it possible to strongly-type these though in TypeScript? Let’s find out.

TypeScript has tuples

Before we figure out how to strongly-type rest parameters, let’s understand tuples. A tuple can be thought of as an array with a fixed number of elements. They are nice for small and obvious data structures. For example, the useState React hook returns a tuple:

const [ state , setState ] = useState ( initialState ) ;

TypeScript lets us define tuples in a type annotation by specifying the type of each element in square brackets. For example:

const tomScore : [ string , number ] : [ "Tom" , 70 ] ;

Open-ended tuples

What have tuples got to do with rest parameters? Well, we’ll get there eventually.

TypeScript lets us have tuples where we can have a varying number of end elements like below:

[ "Tom" , 70 ] [ ( "Jane" , 70 , 60 ) ] [ ( "Fred" , 70 , 60 , 80 ) ] ;

We can specify the type for the above example as [string, ...number[]] .

Strongly-typing rest parameters

I wonder if we can use an open-ended tuple to strongly-type a rest parameter? Let’s try to do this for the scores parameter in the function below:

function logScores ( firstName , ... scores ) { console . log ( firstName , scores ) ; } logScores ( "Ben" , 50 , 75 , 85 ) ;

Let’s try this:

function logScores ( firstName : string , ... scores : [ ... number [ ] ] ) { console . log ( firstName , scores ) ; }

If we think about it, [...number[]] is just number[] . So, this can be simplified to:

function logScores ( firstName : string , ... scores : number [ ] ) { console . log ( firstName , scores ) ; }

… and if we consume the function:

logScores ( "Ben" , 50 , 75 , 85 ) ; logScores ( "Mike" , 90 , 65 , "65" ) ;

Great - it works!

Strongly-typing destructured assignment

Specifying the type on destructured object variables is perhaps not achieved how you might first expect. The following doesn’t specify type annotations for firstName and score :

const fred = { firstName : "Fred" , score : 50 } ; const { firstName : string , score : number } = fred ;

Instead, it specifies names for the destructured variables:

console . log ( firstName ) ; console . log ( score ) ; console . log ( string ) ; console . log ( number ) ;

We specify the type annotation after the destructured object as follows:

const { firstName , score } : { firstName : string ; score : number } = fred ; console . log ( firstName ) ; console . log ( score ) ;

If we are destructuring a tuple, we specify the tuple type after the destructured tuple:

const tomScore : [ string , number ] : [ "Tom" , 70 ] ; const [ firstName , score ] : [ string , number ] = tomScore ; console . log ( firstName ) ; console . log ( score ) ;

We can specify a type on an open-ended tuple as follows:

const tomScores : [ string , ... number [ ] ] = [ "Tom" , 70 , 60 , 80 ] ; const [ firstName , ... scores ] : [ string , ... number [ ] ] = tomScores ; console . log ( firstName ) ; console . log ( scores ) ;

It is worth noting that often TypeScript cleverly infers the types of destructured elements, but it is good to know how to specify type annotation in the edge cases where it doesn’t.

Wrap up

TypeScript tuples are a convenient way of strongly-typing small and obvious data structures. Open-ended tuples can be used to strongly-type rest parameters. TypeScript generally smartly infers the types of destructured elements for us, but when it can’t, we can simply put a type annotation after the destructured items.