Learn TypeScript w/ Mike North

Recent updates to TypeScript

March 22, 2022

Table of Contents

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.

ts
type Color = [
number, // red (0-255)
number, // green (0-255)
number // blue (0-255)
]
Try

For a while, it’s also been possible to use a ...spread[] as the last element of the tuple

ts
// Worked this way, even before TS 4.x
enum Sandwich {
Hamburger,
VeggieBurger,
GrilledCheese,
BLT
}
type SandwichOrder = [
number, // order total
Sandwich, // sandwich
...string[] // toppings
]
 
const order1: SandwichOrder = [12.99, Sandwich.Hamburger, "lettuce"]
const order2: SandwichOrder = [14.99, Sandwich.Hamburger, "avocado", "cheese"]
const order_with_error: SandwichOrder = [
10.99,
"lettuce"
Type 'string' is not assignable to type 'Sandwich'.2322Type 'string' is not assignable to type 'Sandwich'.
]
Try

It has even been possible to use generics for that spread type at the end of the tuple

ts
// Worked this way, even before TS 4.x
type MyTuple<T> = [number, ...T[]]
 
const x1: MyTuple<string> = [4, "hello"]
const x2: MyTuple<boolean> = [4, true]
Try

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)

Attempt to spread an array-like T in a generic tuple type, before TS 4.0

Why does this matter? Let’s look at this example

ts
enum Sandwich {
Hamburger,
VeggieBurger,
GrilledCheese,
BLT
}
type SandwichOrder = [
number, // order total
Sandwich, // sandwich
...string[] // toppings
]
 
const order1: SandwichOrder = [12.99, Sandwich.Hamburger, "lettuce"]
 
/**
* return an array containing everything except the first element
*/
function tail<T>(arg: readonly [number, ...T[]]) {
const [_ignored, ...rest] = arg
return rest
}
 
const orderWithoutTotal = tail(order1)
const orderWithoutTotal: (string | Sandwich)[]
Try

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

ts
function returnArray<T>(arg: readonly T[]): readonly T[] {
return arg
}
const arr = [Sandwich.Hamburger, "lettuce"] as const
const arr: readonly [Sandwich.Hamburger, "lettuce"]
const result = returnArray(arr)
const result: readonly (Sandwich.Hamburger | "lettuce")[]
Try

What we want instead for our tuple scenario, something like this

ts
function returnArray<T extends any[]>(arg: T): T {
return arg
}
const arr: [Sandwich.Hamburger, "lettuce"] = [Sandwich.Hamburger, "lettuce"]
const arr: [Sandwich.Hamburger, "lettuce"]
const result = returnArray(arr)
const result: [Sandwich.Hamburger, "lettuce"]
Try

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;
}
ts
/**
* return an array containing everything except the first element
*/
function tail<T extends any[]>(
arg: readonly [number, ...T]
) {
const [_ignored, ...rest] = arg
return rest
}
const order1: SandwichOrder = [12.99, Sandwich.Hamburger, "lettuce"]
 
const result = tail(order1)
const result: [Sandwich, ...string[]]
Try

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

ts
type MyTuple = [
...[number, number],
...[string, string, string]
]
const x: MyTuple = [1, 2, "a", "b", "c"]
const x: [number, number, string, string, string]
Try

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

ts
type YEScompile1 = [...[number, number], ...string[]]
type NOcompile1 = [...number[], ...string[]]
A rest element cannot follow another rest element.1265A rest element cannot follow another rest element.
 
type YEScompile2 = [boolean, ...number[], string]
Try

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.

ts
class Color {
red // :number no longer needed!
(property) Color.red: number
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]
}
}
Try

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 thrown error with stacktrace

Whereas thrown values of other types (e.g., string) often provide far less information thrown string without stacktrace

I advise always typing errors as unknown, and can’t think of any scenario where it would be worse than an any.

ts
try {
somethingRisky()
} catch (err: unknown) {
if (err instanceof Error) throw err
else throw new Error(`${err}`)
}
Try

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.

ts
type Statistics = {
[K in `${"median" | "mean"}Value`]?: number
}
const stats: Statistics = {}
stats.meanValue
       
Try

You can do some pretty interesting things with these

ts
let winFns: Extract<keyof Window, `set${any}`> = "" as any
let winFns: "setInterval" | "setTimeout"
Try

We even get some special utility types to assist with changing case

ts
type T1 = `send${Capitalize<"mouse" | "keyboard">}Event`
type T1 = "sendMouseEvent" | "sendKeyboardEvent"
type T2 = `send${Uppercase<"mouse" | "keyboard">}Event`
type T2 = "sendMOUSEEvent" | "sendKEYBOARDEvent"
type T3 = `send${Lowercase<"Mouse" | "keyBoard">}Event`
type T3 = "sendmouseEvent" | "sendkeyboardEvent"
Try

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

ts
type Colors = "red" | "green" | "blue"
type ColorSelector = {
[K in Colors as `select${Capitalize<K>}`]: () => void
}
const cs: ColorSelector = {} as any
cs.selectRed()
       
Try

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

ts
// Mike thinks this is way too optimistic
type Dict<T> = { [K: string]: T }
 
const d: Dict<string[]> = {}
d.rhubarb.join(", ") // 💥
Try

My advice was to explicitly type it as

ts
type Dict<T> = { [K: string]: T | undefined }
const d: Dict<string[]> = {}
d.rhubarb.join(", ") // 💥
'd.rhubarb' is possibly 'undefined'.18048'd.rhubarb' is possibly 'undefined'.
Try

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



© 2023 All Rights Reserved