Node.js Now Supports TypeScript By Default
TypeScript is coming to Node 23. Let's break down what that means.
TypeScript's satisfies operator has been out for a while, but it still seems to be a source of confusion that could use some clearing up.
Think of satisfies as another way to assign types to values.
Before we look closer, let's do a review of how to assign types.
First, there's the humble "colon annotation" (we'll go with this slightly medical-sounding name since this concept isn't really given a name in the TS docs).
The : says "this variable is always this type":
const obj: Record<string, string> = {};
obj.id = "123";
When you use a colon annotation, you're declaring that the variable is that type.
That means that the thing you assign to the variable must be that type:
// Type 'number' is not assignable to type 'string'.
const str: string = 123;
It’s possible to give a variable a type that’s wider than the one you initially assign.
let id: string | number = "123";
if (typeof numericId !== "undefined") {
id = numericId;
}
This is useful when you want to have a default which might later be reassigned.
But colon annotations come with an edge-case downside.
When you use a colon, the type BEATS the value.
In other words, if you declare a wider type than you want, you're stuck with the wider type.
For example, in this snippet you don't get autocomplete on the routes object:
const routes: Record<string, {}> = {
"/": {},
"/users": {},
"/admin/users": {},
};
// No error!
routes.awdkjanwdkjn;
This is the problem satisfies was designed to solve.
When you use satisfies
, the value BEATS the type.
This means it infers the narrowest possible type, not the wider type you specify:
const routes = {
"/": {},
"/users": {},
"/admin/users": {},
} satisfies Record<string, {}>;
// Property 'awdkjanwdkjn' does not exist on type
// '{ "/": {}; "/users": {}; "/admin/users": {}; }'
routes.awdkjanwdkjn;
satisfies
also protects you from specifying the wrong thing inside your config object.
So, colon annotations and satisfies
are equally safe.
const routes = {
// Type 'null' is not assignable to type '{}'
"/": null,
} satisfies Record<string, {}>;
Another way you can assign types to variables is the ‘as
’ annotation.
Unlike satisfies
and colon annotations, using ‘as
’ annotations lets you lie to TypeScript.
In this example, you wouldn’t see an error in your IDE but it will break at runtime:
type User = {
id: string;
name: {
first: string;
last: string;
};
};
const user = {} as User;
// No error! But this will break at runtime
user.name.first;
There are some limits to the lies– you can add properties to objects, but you can’t convert between basic types.
For instance, you can’t force TypeScript to convert a string into a number…
…unless you use the monstrous ‘as-as’:
// Conversion of type 'string' to type 'number'
// may be a mistake because neither type
// sufficiently overlaps with the other.
const str = "my-string" as number;
const str2 = "my-string" as unknown as number;
Here’s a legit usage of as
, where it is used to convert an object to a known type that hasn’t been constructed yet:
type User = {
id: string;
name: string;
};
// The user hasn't been constructed yet, so we need
// to use 'as' here
const userToBeBuilt = {} as User;
(["name", "id"] as const).forEach((key) => {
// Assigning to a dynamic key!
userToBeBuilt[key] = "default";
});
A word of caution: if you’re using as
as your default way to annotate variables, that’s almost certainly wrong!
The code below might look safe, but as soon as you add another property to the User
type, the defaultUser
will be out of date, and it will not show you an error!
type User = {
id: string;
name: string;
};
const defaultUser = {
id: "123",
name: "Matt",
} as User;
There’s one last way to give a type to a variable:
Don’t.
That’s not a typo!
TypeScript does an amazing job at inferring types for your variables.
In fact most of the time, you won’t need to type your variables at all:
const routes = {
"/": {},
"/users": {},
"/admin/users": {},
};
// OK!
routes["/"];
// Property 'awdahwdbjhbawd' does not exist on type
// { "/": {}; "/users": {}; "/admin/users": {}; }
routes["awdahwdbjhbawd"];
To recap, we’ve got FOUR ways of assigning a type to a variable:
satisfies
as
annotationsWith different ways to do similar things, it can get a bit confusing about when to use what.
Simple use cases are where satisfies
works best:
type User = {
id: string;
name: string;
};
const defaultUser = {
id: "123",
name: "Matt",
} satisfies User;
But most of the time, when you want to assign a type to a variable you probably want the type to be wider.
If this example used satisfies
, you wouldn’t be able to assign numericId
to id
:
// colon annotation
let id: string | number = "123";
if (typeof numericId !== "undefined") {
id = numericId;
}
// satisfies
let id = "123" satisfies string | number;
if (typeof numericId !== "undefined") {
// Type 'number' is not assignable to type 'string'.
id = numericId;
}
The rule of thumb is that you should only use satisfies in two specific situations:
Total TypeScript Core Volume includes dozens of challenges in this vein that will help you solidify your mental model for type assignments and transformations!
Clarifying the `satisfies` Operator
TypeScript is coming to Node 23. Let's break down what that means.
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.