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`