This Crazy Syntax Lets You Get An Array Element's Type
Learn how to extract the type of an array element in TypeScript using the powerful Array[number]
trick.
TypeScript 5.5 will be dropping in the next couple of months. And thanks to one PR in particular, I already know it's going to be an incredible release.
On Friday night, TypeScript merged Dan Vanderkam's PR to "Infer type predicates from function bodies using control flow analysis". Let's explain exactly what that PR changed, and why it's such a big deal.
Let's say you want to check if a value is a string or a number. This is pretty simple - you can use an if
statement:
console .log (value );
if (typeof value === "string") {
console .log (value );}
Now, inside the if
statement, TypeScript knows that value
is a string. This is called "narrowing" - TypeScript has narrowed the type of value
from string | number
to just string
.
But what if you want to write a function that does this? You might write something like this:
function isString (value : unknown) {
return typeof value === "string";
}
Then, you'd apply the function to value
- but something's not quite right:
if (isString(value)) {
console.log(value); // string | number
}
We've lost our narrowing! TypeScript doesn't know that isString
is a type predicate - a function that narrows the type of its argument.
We can fix this by adding a return type annotation to isString
:
function isString (value : unknown): value is string {
return typeof value === "string";
}
if (isString (value )) {
console .log (value );}
Now, we have our narrowing back. We can even use this function in a filter
call:
function isString(value: unknown): value is string {
return typeof value === "string";
}
// ---cut---
const arr = [1, "hello", 3, "world"];
const strings = arr.filter(isString);
// ^?
But this is a bit of a pain. Even worse than that, the return type annotation can get out of sync with the implementation of the function. We can change the return type to value is number
and TypeScript won't complain:
function isString (value : unknown): value is number {
return typeof value === "string";
}
if (isString (value )) {
console .log (value );}
This makes this annotation quite brittle.
Wouldn't it be great if value is string
could be inferred from the function body? That way, it would stay in sync with the implementation of the function, and we wouldn't have to write it out ourselves.
That's exactly what Dan's PR does. With TypeScript 5.5, you can write an isString
function, and it'll infer the type predicate from the function body:
function isString(value: unknown) {
return typeof value === "string";
}
if (isString(value)) {
console.log(value); // string
}
This uses TypeScript's control flow analysis to infer the type predicate from the function body. This is the same analysis that's used to narrow types inside if
statements.
This means that you can do complex type narrowing without having to write out the type predicate yourself.
function isObjAndHasIdProperty(value: unknown) {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof value.id === "number"
);
}
if (isObjAndHasIdProperty(value)) {
console.log(value.id); // number
}
This is a huge win for TypeScript. It fixes a long-running issue with TypeScript's inference. It makes capturing narrowing logic in functions much easier. And it makes TypeScript's type system more powerful.
Honestly, I can't believe they shipped it. It's a huge change to how TypeScript works, and it's going to make a lot of people's development lives easier.
filter(Boolean)
Work?One question I got when I tweeted about this was whether filter(Boolean)
would automatically infer.
I'll need to test it out when a beta is released, but on balance I think not. Boolean
is not, by itself, a type predicate. For instance, if you try to use it in an if
statement to remove a null
from a union, TypeScript won't infer the type:
declare const value: string | null;
// ---cut---
if (Boolean(value)) {
console.log(value);
// ^?
}
So, it also won't work with .filter
. This means that you'll still need to use my library ts-reset
to get the correct inference:
import "@total-typescript/ts-reset";
const arr = [null, "hello", null, "world"];
const strings = arr.filter(Boolean); // string[]
Share this article with your friends
Learn how to extract the type of an array element in TypeScript using the powerful Array[number]
trick.
Learn how to publish a package to npm with a complete setup including, TypeScript, Prettier, Vitest, GitHub Actions, and versioning with Changesets.
Enums in TypeScript can be confusing, with differences between numeric and string enums causing unexpected behaviors.
Is TypeScript just a linter? No, but yes.
It's a massive ship day. We're launching a free TypeScript book, new course, giveaway, price cut, and sale.
Learn why the order you specify object properties in TypeScript matters and how it can affect type inference in your functions.