Variadic Tuple Types
We know that a tuple type is an ordered collection (often of known length), with the type of each member known as well.
tsTry
typeColor = [number, // red (0-255)number, // green (0-255)number // blue (0-255)]
For a while, it’s also been possible to use a ...spread[]
as the last
element of the tuple
tsTry
// Worked this way, even before TS 4.xenumSandwich {Hamburger ,VeggieBurger ,GrilledCheese ,BLT }typeSandwichOrder = [number, // order totalSandwich , // sandwich...string[] // toppings]constorder1 :SandwichOrder = [12.99,Sandwich .Hamburger , "lettuce"]constorder2 :SandwichOrder = [14.99,Sandwich .Hamburger , "avocado", "cheese"]constorder_with_error :SandwichOrder = [10.99,Type 'string' is not assignable to type 'Sandwich'.2322Type 'string' is not assignable to type 'Sandwich'."lettuce" ]
It has even been possible to use generics for that spread type at the end of the tuple
tsTry
// Worked this way, even before TS 4.xtypeMyTuple <T > = [number, ...T []]constx1 :MyTuple <string> = [4, "hello"]constx2 :MyTuple <boolean> = [4, true]
It’s important to note that, before TS 4.0 we had to use ...T[]
, and could not
do something like this (example in TS playground)
Why does this matter? Let’s look at this example
tsTry
enumSandwich {Hamburger ,VeggieBurger ,GrilledCheese ,BLT }typeSandwichOrder = [number, // order totalSandwich , // sandwich...string[] // toppings]constorder1 :SandwichOrder = [12.99,Sandwich .Hamburger , "lettuce"]/*** return an array containing everything except the first element*/functiontail <T >(arg : readonly [number, ...T []]) {const [_ignored , ...rest ] =arg returnrest }constorderWithoutTotal =tail (order1 )
This is not ideal. A (string | Sandwich)[]
is not the same thing as a [Sandwich, ...string[]]
.
What we’re seeing here is what happens when TS tries to infer the following
ts
T[] <-----> [Sandwich.Hamburger, "lettuce"]
similar to what you’d see here
tsTry
functionreturnArray <T >(arg : readonlyT []): readonlyT [] {returnarg }constarr = [Sandwich .Hamburger , "lettuce"] asconst constresult =returnArray (arr )
What we want instead for our tuple scenario, something like this
tsTry
functionreturnArray <T extends any[]>(arg :T ):T {returnarg }constarr : [Sandwich .Hamburger , "lettuce"] = [Sandwich .Hamburger , "lettuce"]constresult =returnArray (arr )
Inference is doing a lot more for us here, and I would argue, we’re no longer
losing type information, once T = [Sandwich.Hamburger, "lettuce"]
TS 4.0 introduces support for variadic tuples. This relaxes the limitation
shown above, and allows us to use ...T
in tuple types. Going back to our tail
example, let’s make a small change
diff
-function tail<T>(arg: readonly [number, ...T[]]) {+function tail<T extends any[]>(arg: readonly [number, ...T]) {const [_ignored, ...rest] = arg;return rest;}
tsTry
/*** return an array containing everything except the first element*/functiontail <T extends any[]>(arg : readonly [number, ...T ]) {const [_ignored , ...rest ] =arg returnrest }constorder1 :SandwichOrder = [12.99,Sandwich .Hamburger , "lettuce"]constresult =tail (order1 )
There we go! This improved degree of inference now works for us inside the tuple!
We can also now use more than one ...spread
in a single tuple
tsTry
typeMyTuple = [...[number, number],...[string, string, string]]constx :MyTuple = [1, 2, "a", "b", "c"]
It’s important to note that only one ...rest[]
element is possible in a given
tuple, but it doesn’t necessarily have to be the last element
tsTry
typeYEScompile1 = [...[number, number], ...string[]]typeA rest element cannot follow another rest element.1265A rest element cannot follow another rest element.NOcompile1 = [...number[], ...string[]]typeYEScompile2 = [boolean, ...number[], string]
Check out how this feature has allowed the Rx.js project to simplify their types
Class Property Inference from Constructors
This major convenience feature reduces the need for class
field type annotations by inferring their types from
assignments in the constructor
. It’s important to remember
that this only works when noImplicitAny
is set to true
.
tsTry
classColor {red // :number no longer needed!green // :number no longer needed!blue // :number no longer needed!constructor(c : [number, number, number]) {this.red =c [0]this.green =c [1]this.blue =c [2]}}
Thrown values as unknown
Before TS 4.0, thrown values were always considered to be of type
any
. Now, we can choose to regard it as of type unknown
.
If you’ve ever found it risky to assume that a message
, stacktrace
,
or name
property is on every possible thrown value you encounter
in a catch clause, this feature may make help you sleep a little more soundly.
Thrown errors provide a nice stack trace
Whereas thrown values of other types (e.g., string
) often provide far less
information
I advise always typing errors as unknown
, and can’t think of any scenario
where it would be worse than an any
.
tsTry
try {somethingRisky ()} catch (err : unknown) {if (err instanceofError ) throwerr else throw newError (`${err }`)}
There’s also a useUnknownInCatchVariables
compilerOption
flag that
will make thrown values unknown across your entire project
Template literal types
You can think of these like template strings, but for types.
tsTry
typeStatistics = {[K in `${"median" | "mean"}Value`]?: number}conststats :Statistics = {}stats .meanValue
You can do some pretty interesting things with these
tsTry
letwinFns :Extract <keyofWindow , `set${any}`> = "" as any
We even get some special utility types to assist with changing case
tsTry
typeT1 = `send${Capitalize <"mouse" | "keyboard">}Event`typeT2 = `send${Uppercase <"mouse" | "keyboard">}Event`typeT3 = `send${Lowercase <"Mouse" | "keyBoard">}Event`
Key remapping in mapped types
You may recall that mapped types are kind of like our “for loop” to build up an object type by key-value pairs. Before TS 4.1, our ability to transform keys was very limited (usually involving an explicit “old key to new key mapping”)
We now have some new syntax (note the as
in the example below) that
lets us transform keys in a more declarative way. This language
feature works quite nicely with template literal types
tsTry
typeColors = "red" | "green" | "blue"typeColorSelector = {[K inColors as `select${Capitalize <K >}`]: () => void}constcs :ColorSelector = {} as anycs .selectRed ()
Checked index access
If you’ve ever heard me rant about typing Dictionaries, you may recall that
my advice to describe them as having a possibility of holding undefined
under some keys
tsTry
// Mike thinks this is way too optimistictypeDict <T > = { [K : string]:T }constd :Dict <string[]> = {}d .rhubarb .join (", ") // 💥
My advice was to explicitly type it as
tsTry
typeDict <T > = { [K : string]:T | undefined }constd :Dict <string[]> = {}'d.rhubarb' is possibly 'undefined'.18048'd.rhubarb' is possibly 'undefined'.. d .rhubarb join (", ") // 💥
Great, now we see an error alerting us to the ~possibility~ certainty that
there is no string[]
stored under the rhubarb
key.
However, it’s tough to be vigilant enough to remember to do this to every index signature in your entire app.
TypeScript now gives us a compiler flag that will do this for us: noUncheckedIndexAccess
.
Sadly we can’t demonstrate how this works using these docs yet, but here’s an example from the typescript playground