Combining TypeScript satisfies operator and const assertion
TLDR
Combine const
assertion and the satisfies
operator with the
as const satisfies SomeType
syntax:
type Metadata = string | number | { [key: string]: Metadata };
type Profile = Record<string, Metadata>;
const profile = {
name: 'topher',
age: 2,
status: 'active',
} as const satisfies Profile;
profile.status; // ✅ type inferred as string!
profile.age = 2; // 🚫 error! age is read-only
Why satisfies
?
TypeScript added the satisfies
operator in the
4.9 release
to improve type inference and type safety. satisfies
allows TypeScript to
infer types for object’s and array’s while providing type safety using an
explicit type.
For example, say we have the following types:
type Metadata = string | number | { [key: string]: Metadata };
type Profile = Record<string, Metadata>;
And an object literal representing a user profile:
const profile = {
name: 'topher',
age: 2,
status: 'active',
};
As written, profile
’s type is inferred which gives us great editor support and
autocomplete:
profile.age; // ✅ number, great!
On the downside, there’s no relationship between the Profile
type and the
profile
object so we won’t get type errors if the profile
object is
incompatible with Profile
type.
Before satisfies
Before TypeScript 4.9, we could type the profile
object with:
- an explicit annotation:
const profile: Profile = ...
- or a
type assertion:
const profile = ... as Profile
Types via explicit type annotation
Adding an explicit type annotation (i.e. const profile: Profile = ...
) helps
us catch errors if the profile
object differs from the Profile
type:
const profile: Profile = {
name: 'toph',
age: 21,
status: 'active',
groups: [], // 🚫 error! not a member of `Profile`. Thanks TypeScript 🥰
};
The downside TypeScript no longer infers types from the object literal so our type info isn’t as helpful:
// exists but type is `Metadata` | `undefined` 😭
console.log(profile.status);
Types via type assertion
Another option is to use a type assertion with the
const profile = {/* ... */} as Profile
syntax:
const profile = {
name: 'toph',
age: 21,
status: 'active',
} as Profile;
This method has the same downside as using an explicit type annotation (i.e. we lose type inference) and exposes us to potential runtime errors. This happens because TypeScript treats the assertion as valid, even when it may not be.
Using satisfies
operator
We can instead use satisfies
to add types:
const profile = {
name: 'toph',
age: 21,
status: 'active',
} satisfies Profile;
While retaining inferred types:
console.log(profile.age); // ✅ number
console.log(profile.status); // ✅ string
And providing type safety:
const profile = {
name: 'toph',
age: 21,
status: 'active',
groups: ['foo'], // 🚫 string[] is not assignable to type `Metadata`
} satisfies Profile;
const
assertions
TypeScript 3.4 introduced const
assertions
as a way to:
- prevent type widening
- type all object properties
readonly
- type array’s as readonly tuples
Combining const
assertion and satisfies
We can combine const
assertions and satisfies
using the
as const satisfies SomeType
syntax:
const profile = {
name: 'topher',
age: 2,
status: 'active',
} as const satisfies Profile;
This gives us the best type inference and immutability:
console.log(profile.age); // ✅ number;
profile.groups = ['foo']; // 🚫 string[] is not assignable to type `Metadata`