The new Instantiation Expression in Typescript
With instantiation expressions, we can now take functions and constructors and feed them type arguments directly.
TLDR
Instantiation expressions provide the ability to specify type arguments for generic functions or generic constructors without actually calling them.
Introduction
You’re building a banking application and need to write a certain withdrawMoney
function that takes the currency
and amount
to be withdrawn as parameters.
Here’s a contrived implementation anyone could arguably come up with:
const withdrawMoney = (currency: string, amount: number) => {
// do something
return { currency, amount }
}
Assuming you want the type of the currency
and amount
to be ultimately flexible, you may go ahead and represent the arguments with generics as follows:
const withdrawMoney = <C, A>(currency: C, amount: A) => {
// do something
return { currency, amount }
}
Looking good! Heck, you may win an award at work for such fine code.
The Specificity Problem
When you have generic functions as in the example above, more often than not, you may find yourself in a situation where you need a stricter or more specific version of the same function.
For example, withdrawMoney
is your generic handler for withdrawing any sum of money in any currency.
However, you may find yourself requiring a more specific version that handles tipping creators one dollar (creators deserve more).
To make a more specific function, there used to be only two options.
1. Wrap the generic function in a new function
This is straightforward.
You’d write a new function tipCreator
that composes the withdrawMoney
function as follows:
const tipCreator = (currency: 'USD', amount: 1) => {
return withdrawMoney(currency, amount)
}
And of course, the return value of tipCreator
is correctly typed, leveraging the type of value passed to withdrawMoney
.
This isn’t a bad solution.
However, it seems a bit too much. All we want is to restrict the generic type for the withdrawMoney
function. Do we really need to re-invoke withdrawMoney
to get the appropriate type here?
Perhaps not!
Ideally, we would prefer a solution that allows you to aliaswithdrawMoney
while replacing all the generics in its signature.
2. Use an explicit type for an alias
An alternative solution would be to use an explicit type for a new function, as seen below:
const tipCreator: (currency: 'USD', amount: 1) =>
{ currency: 'USD', amount: 1 } = withdrawMoney
If you remove the type information, here’s what this is stripped down to:
const tipCreator = withdrawMoney
Essentially, an alias.
The difference is the extra explicit type.
This solution works, but writing explicit types as seen above can get unwieldy rapidly.
An ideal solution would be something close to the stripped down version above, without the fuss of a full on explicit type.
Introducing instantiation expressions
Now that you’re familiar with the problem, here’s the solution with instantiation expressions:
// look here 👇
const tipCreator = withdrawMoney<'USD', 1>
For completeness, here’s the definition for withdrawMoney
:
const withdrawMoney = <C, A>(currency: C, amount: A) => {
// do something
return { currency, amount }
}
How easy!
With instantiation expressions, we can now take functions and constructors and feed them type arguments directly.
Remember that this also works for constructors e.g., Array
, Map
or Set
e.g.,
type Currency = 'USD' | 'EUR' | 'GBP'
// use instantiation expressions
const CurrencyMap = Map<string, Currency>;
// use the new alias
const currencyMap = new CurrencyMap()
// currencyMap will have type Map<string, Currency>
There you go!
How do I know when you use instantiation expressions?
Next time you find yourself needing a specifically typed version of a function or constructor, it may be a great time to consider instantiation expressions.
Further resources
- The Instantiation expressions implementation
- The Seven Most Stackoverflowed TS Questions (PDF)
- Build Strongly Typed Polymorphic React Components (PDF)