ยท new features

What is the satisfies operator in TypeScript?

TypeScript 4.9 introduces the "satisfies" operator, which allows us to verify that the type of an expression matches a specific type. This operator can be used to narrow down a union type and provide more precise type checking. In the example given, the "satisfies" operator is used to restrict the keys and values of a record type.

The introduction of the satisfies operator in TypeScript 4.9 enables us to verify that the type of an expression corresponds to a specific type. It can offer greater precision compared to a type annotation and assist in narrowing down a union type.

Contents

Example

type TeamMembers = string | string[];
 
type TeamNames = 'Bulletproof' | 'Iconic';
 
type Teams = Record<TeamNames, TeamMembers>;
 
const AllTeams = {
  Bulletproof: ['Alex', 'Lara', 'Sofia'],
  Iconic: 'Benny',
};

In the given example, we define a record named Teams that permits keys of the TeamNames type and values of the TeamMembers type. The TeamMembers type represents a union of either a string or a string array. Our desired outcome for the AllTeams constant is to restrict the keys to the potential TeamNames options while offering string or string array methods for the values, depending on whether the value is an array or not.

Problems with Type Annotation

When using a type annotation for AllTeams, we encounter a limitation in accessing specific methods based on the value types. Since the value type of the Record is a union, we are restricted to using intersecting functionality only:

type TeamMembers = string | string[];
 
type TeamNames = 'Bulletproof' | 'Iconic';
 
type Teams = Record<TeamNames, TeamMembers>;
 
const AllTeams: Teams = {
  Bulletproof: ['Alex', 'Lara', 'Sofia'],
  Iconic: 'Benny',
};
 
// TS2339: Property 'join' does not exist on type 'TeamMembers'.
console.log(AllTeams.Bulletproof.join(', '));
 
// TS 2339: Property 'toUpperCase' does not exist on type 'TeamMembers'.
console.log(AllTeams.Iconic.toUpperCase());

Problems with Type Inference

When relying on type inference, our teams Bulletproof and Iconic will receive appropriate type inferences, so we can use their specific methods. The downside is that we no longer have restrictions on the TeamNames, which grants us the ability to introduce arbitrary names, such as IsNotAllowed:

type TeamMembers = string | string[];
 
type TeamNames = 'Bulletproof' | 'Iconic';
 
type Teams = Record<TeamNames, TeamMembers>;
 
const AllTeams = {
  Bulletproof: ['Alex', 'Lara', 'Sofia'],
  Iconic: 'Benny',
  // Key is not part of "TeamNames"
  IsNotAllowed: 'Bob',
};
 
// ok!
console.log(AllTeams.Bulletproof.join(', '));
 
// ok!
console.log(AllTeams.Iconic.toUpperCase());

Solution with Satisfies Operator

When using the satisfies operator, TypeScript will infer our AllTeams constant based on the Teams type and additionally narrow the TeamMembers union, allowing us to use specific methods associated with the inferred type:

type TeamMembers = string | string[];
 
type TeamNames = 'Bulletproof' | 'Iconic';
 
type Teams = Record<TeamNames, TeamMembers>;
 
const AllTeams = {
  Bulletproof: ['Alex', 'Lara', 'Sofia'],
  Iconic: 'Benny',
  // Ok, no other keys possible!
} satisfies Teams;
 
// ok!
console.log(AllTeams.Bulletproof.join(', '));
// ok!
console.log(AllTeams.Iconic.toUpperCase());

The satisfies operator helps in inferring the type, enhancing the developer experience during design time. It does not generate any additional JavaScript code. The resulting JavaScript output for our example will be as follows:

'use strict';
const AllTeams = {
  Bulletproof: ['Alex', 'Lara', 'Sofia'],
  Iconic: 'Benny',
  // Ok, no other keys possible!
};
// ok!
console.log(AllTeams.Bulletproof.join(', '));
// ok!
console.log(AllTeams.Iconic.toUpperCase());

Alternative Solution

If you are using a TypeScript version below 4.9, you can solve the initial use case by defining an object literal type for Teams. This approach offers less flexibility compared to defining a union for the values and relying on TypeScript to infer the specific type. Despite this limitation, using an object literal type will still enforce constraints on the key names and provide access to the specific methods available for the values:

type Teams = {
  Bulletproof: string[];
  Iconic: string;
};
 
const AllTeams: Teams = {
  Bulletproof: ['Alex', 'Lara', 'Sofia'],
  Iconic: 'Benny',
};
 
console.log(AllTeams.Bulletproof.join(', '));
console.log(AllTeams.Iconic.toUpperCase());
Back to Blog