satisfies
operatorTypeScript’s satisfies
operator lets us check the type of a value (mostly) without influencing it. In this blog post, we examine how exactly it works and where it’s useful.
For showing computed and inferred types in source code, I use the npm package asserttt
– e.g.:
// Types of values
assertType<string>('abc');
assertType<number>(123);
// Equality of types
type Pair<T> = [T, T];
type _ = Assert<Equal<
Pair<string>, [string,string]
>>;
satisfies
operator? The satisfies
operator enforces at compile time that a given value
is assignable to a given Type
:
value satisfies Type
value
. This operator has no effect at runtime.value
is usually unchanged. There are a few exceptions, though, which we’ll look at later.satisfies
This is not allowed:
const sayHello = (name) => `Hello ${name}!`
// @ts-expect-error: Cannot find name 'satisfies'.
satisfies (str: string) => string;
Parentheses can help:
const sayHello = (
(name) => `Hello ${name}!`
) satisfies (str: string) => string;
Let’s examine how various annotations affect the type of an object literal. We start without any annotations:
const point1 = { x: 2, y: 5 };
assertType<
{ x: number, y: number }
>(point1);
TypeScript generalizes the types of .x
and .y
to number
. That changes if we use the as const
annotation:
const point2 = { x: 2, y: 5 } as const;
assertType<
{ readonly x: 2, readonly y: 5 }
>(point2);
Now both properties are read-only and have narrow types. What happens if we want to check that the shape of our point is correct, via a new type Point
?
type Point = { x: number, y: number };
const point3: Point = { x: 2, y: 5 } as const;
assertType<
Point
>(point3);
point3
once again has a broader type. TypeScript infers the type name Point
, which is an alias for the type that point1
had. We declared that variable without any type-level annotations.
satisfies
lets us check that our point has the correct shape without discarding the narrow type that as const
gives us:
const point4 = { x: 2, y: 5 } as const satisfies Point;
assertType<
{ readonly x: 2, readonly y: 5 }
>(point4);
How is satisfies
different from as
? On one hand, as
generally changes the type of its left-hand side. On the other hand, it doesn’t type-check as thoroughly as satisfies
:
// Should warn about missing property but doesn’t!
const point5 = { x: 2 } as const as Point; // OK
assertType<
Point
>(point5);
In contrast, satisfies
warns us if we omit property .y
:
// @ts-expect-error: Type '{ readonly x: 2; }' does not satisfy
// the expected type 'Point'.
const point6 = { x: 2 } as const satisfies Point;
Why do we want the type of a point to be narrow? Actually, often we are perfectly fine with broader types. But there are use cases that require them to be narrow. We’ll look at those next.
The following code demonstrates that partialPoint1
having the broader type PartialPoint
makes it less pleasant to work with:
type PartialPoint = { x?: number, y?: number };
const partialPoint1: PartialPoint = { y: 7 };
// Should be an error
const x1 = partialPoint1.x;
assertType<number | undefined>(x1);
// Type should be `number`
const y1 = partialPoint1.y;
assertType<number | undefined>(y1);
This is what happens if we use satisfies
:
const partialPoint2 = { y: 7 } satisfies PartialPoint;
// @ts-expect-error: Property 'x' does not exist on type
// '{ y: number; }'.
const x2 = partialPoint2.x;
const y2 = partialPoint2.y;
assertType<number>(y2);
The following object implements an enum for text styles:
const TextStyle = {
Bold: {
html: 'b',
latex: 'textbf',
},
Italics: {
html: 'i',
// Missing: latex
},
};
type TextStyleKeys = keyof typeof TextStyle;
type _ = Assert<Equal<
TextStyleKeys, "Bold" | "Italics"
>>;
TextStyleKeys
with property keys.TextStyle.Italics.latex
and TypeScript didn’t warn us.Let’s use the following type to check property values:
type TTextStyle = {
html: string,
latex: string,
};
Our first attempt looks like this:
const TextStyle: Record<string, TTextStyle> = {
Bold: {
html: 'b',
latex: 'textbf',
},
// @ts-expect-error: Property 'latex' is missing in type
// '{ html: string; }' but required in type 'TTextStyle'.
Italics: {
html: 'i',
},
};
type TextStyleKeys = keyof typeof TextStyle;
type _ = Assert<Equal<
TextStyleKeys, string
>>;
TextStyleKeys
is now string
.satisfies
Once again, satisfies
can help us:
const TextStyle = {
Bold: {
html: 'b',
latex: 'textbf',
},
// @ts-expect-error: Property 'latex' is missing in type
// '{ html: string; }' but required in type 'TTextStyle'.
Italics: {
html: 'i',
},
} satisfies Record<string, TTextStyle>;
type TextStyleKeys = keyof typeof TextStyle;
type _ = Assert<Equal<
TextStyleKeys, "Bold" | "Italics"
>>;
Now we get a warning for the missing property and a useful type TextStyleKeys
.
In the previous example, we checked the shapes of property values. Sometimes we are additionally dealing with a limited set of property keys and would like to check those – to avoid typos etc.
The following example is inspired by the pull request for satisfies
and uses the type ColorName
to check property keys and the type Color
to check property values. We get an error because they key 'blue'
is missing:
type ColorName = 'red' | 'green' | 'blue';
type Color =
| string // hex
| [number, number, number] // RGB
;
const fullColorTable = {
red: [255, 0, 0],
green: '#00FF00',
// @ts-expect-error:
// Type '{ red: [number, number, number]; green: string; }'
// does not satisfy the expected type 'Record<ColorName, Color>'.
} satisfies Record<ColorName, Color>;
What if we don’t want to use all of the keys but still get checks for typos? That can be achieved by making the record for the object partial (line A):
const partialColorTable = {
red: [255, 0, 0],
// @ts-expect-error: Object literal may only specify known
// properties, but 'greenn' does not exist in type
// 'Partial<Record<ColorName, Color>>'.
// Did you mean to write 'green'?
greenn: '#00FF00',
} satisfies Partial<Record<ColorName, Color>>; // (A)
We can also extract the property keys:
const partialColorTable2 = {
red: [255, 0, 0],
green: '#00FF00',
} satisfies Partial<Record<ColorName, Color>>;
type PropKeys = keyof typeof partialColorTable2;
type _ = Assert<Equal<
PropKeys, "red" | "green"
>>;
Consider the following function call:
JSON.stringify({ /*···*/ });
The parameter of JSON.stringify()
has the type any
. How can we ensure on our end that the object we pass to it has the right shape (no typos in the property keys etc.)? One option is to create a variable for the object:
const obj: SomeType = { /*···*/ };
JSON.stringify(obj);
Another option is to use satisfies
:
JSON.stringify({ /*···*/ } satisfies SomeType);
The following subsections demonstrate this technique for several use cases.
fetch()
The following code was inspired by an article by Matt Pocock: We are using fetch()
to post data to an API.
type Product = {
name: string,
quantity: number,
};
const response = await fetch('/api/products', {
method: 'POST',
body: JSON.stringify(
{
name: 'Toothbrush',
quantity: 3,
} satisfies Product
),
headers: {
'Content-Type': 'application/json',
},
});
export default
Inline named exports can have type annotations:
import type { StringCallback } from './core.js';
export const toUpperCase: StringCallback =
(str) => str.toUpperCase();
With a default export, there is no way to add one but we can use satisfies
:
import type { StringCallback } from './core.js';
export default (
(str) => str.toUpperCase()
) satisfies StringCallback;
Consider the following class that implements the interface JsonInstance
for converting instances to JSON:
class Person implements JsonInstance {
static fromJson(json: unknown): Person {
if (typeof json !== 'string') {
throw new TypeError();
}
return new Person(json);
}
name: string;
constructor(name: string) {
this.name = name;
}
toJson(): unknown {
return this.name;
}
}
/** Converting instances to JSON */
interface JsonInstance {
toJson(): unknown;
}
The functionality for converting JSON to instances via Person.fromJson()
is already there; we’d like to enforce and check it via the following interface:
/** Converting JSON to instances */
interface JsonStatic {
fromJson(json: unknown): JsonInstance;
}
This doesn’t work because it hides the constructor and all properties except for the one in the interface:
const Person: JsonStatic = class implements JsonInstance {
// ···
};
This is one option, but it is verbose and introduces unnecessary runtime code:
const personImplementsJsonStatic: JsonStatic = Person;
satisfies
provides us with an elegant solution:
const Person = class implements JsonInstance {
// ···
} satisfies JsonStatic;
satisfies
isn’t always needed In this section, we look at code where it seems like we would need satisfies
, but TypeScript already correctly narrows the code.
string
and number
In the following code, we don’t need to use satisfies
for str1
and str2
– they are already narrowed to string
:
type StrOrNum = string | number;
const str1 = 'abc' as StrOrNum;
// @ts-expect-error: Property 'toUpperCase' does not exist on
// type 'StrOrNum'.
str1.toUpperCase();
const str2: StrOrNum = 'abc';
str2.toUpperCase(); // OK
let str3: StrOrNum = 'abc';
str3.toUpperCase(); // OK
In the next example, linkToIntro
is also narrowed to one element of the discriminated union LinkHref
:
type LinkHref =
| {
kind: 'LinkHrefUrl',
url: string,
}
| {
kind: 'LinkHrefId',
id: string,
}
;
const linkToIntro: LinkHref = {
kind: 'LinkHrefId',
id: '#intro',
};
// Type was narrowed:
assertType<
{
kind: 'LinkHrefId',
id: string,
}
>(linkToIntro);
satisfies
can change inferred types While satisfies
usually does not change the inferred type of a value, there are exceptions.
type Robin = {
name: 'Robin',
};
const robin1 = { name: 'Robin' };
assertType<string>(robin1.name);
const robin2 = { name: 'Robin' } satisfies Robin;
assertType<'Robin'>(robin2.name);
// No `satisfies`
const tuple1 = ['a', 1];
assertType<
(string | number)[]
>(tuple1);
// Non-empty tuple
const tuple2 = ['a', 1] satisfies [unknown, ...unknown[]];
assertType<
[string, number]
>(tuple2);
// Any tuple
const tuple3 = [] satisfies [unknown?, ...unknown[]];
assertType<
[]
>(tuple3);
// Any tuple
const tuple4 = ['a', 1] satisfies [] | unknown[];
assertType<
[string, number]
>(tuple4);
satisfies
does not change explicit types const tuple1: Array<string | number> = ['a', 1];
// @ts-expect-error: Type '(string | number)[]' does not satisfy
// the expected type '[unknown, ...unknown[]]'.
const tuple2 = tuple1 satisfies [unknown, ...unknown[]];
TypeScript does have an operation at the type level that is similar to satisfies
, but we can implement it ourselves:
type Satisfies<Type extends Constraint, Constraint> = Type;
type T1 = Satisfies<123, number>; // 123
type _ = Assert<Equal<
T1, 123
>>;
// @ts-expect-error: Type 'number' does not satisfy
// the constraint 'string'.
type T2 = Satisfies<123, string>;
Source of this blog post: