satisfies TypeScript keyword
satisfies is a keyword introduced in TypeScript 4.9, in late 2022.
satisfies is used when you want to validate that an expression matches a type, without also casting that expression to that type.
It's a bit confusing.
TLDR
I don't think satisfies is generally necessary. What it does is not immediately obvious, and can be done more explicitly with regular type narrowing.
See Type Guards.
The problem satisfies solves
For an example, check out this:
type Color = 'red' | 'blue' | 'green';
type Circle = {
radius: number;
color?: Color;
}
const dot = {
size: 10,
color: 'red',
};
function paint(color: Color) {
console.log(`Painting ${color} paint everywhere`);
}
// This throws an error! 😵
paint(dot.color);This throws an error since the paint() function expects exactly the strings "red" or "blue" or "green".
In this case, dot has no type narrowing or validation, so has this type:
const dot: {
size: number;
color: string;
}TypeScript will infer broadly by default.
string does not match Color, so we get a type error.
There is a second problem, in the dot object we have the property size, when it should have been radius!
There are tons of ways we could solve this.
Using type assertions
The first way you might think of would be an assertion. We can assert in two different ways:
- Type casting using
as
type Circle = {
radius: number;
color?: Color;
}
const dot = {
// New type error! This one we want, since this is the wrong property name.
size: 10,
color: 'red'
} as Circle;
// However, this still errors! 😵
paint(dot.color):- Type annotation
const dot: Circle = {
size: 10,
color: 'red'
};Both of these have the same issue. The type of Circle.color is Color | undefined.
However, our paint() function needs a Color. Not a Color | undefined! So it errors out.
const assertion
We can use the as const construct introduced in TypeScript 3.4 instead here.
The as const construct will narrow the type of an expression to its narrowest possible interpretation.
This means that all properties on any object will be marked as readonly and be narrowed to their literal type.
In this case, we can either assert on dot or just on dot.color:
// Either of these do get the job done
const dot = {
size: 10,
color: 'red' as const,
};
// Or
const dot = {
size: 10,
color: 'red',
} as const;
// No more type error 😌
paint(dot.color);In both of these cases, the type of dot.color becomes readonly "red".
Since readonly "red" satisfies "red" | "blue" | "green", this all passes.
However, maybe marking everything as readonly is either undesirable or impossible for your expression.
satisfies
Using satisfies we can check that the dot object conforms to the Circle type without overly casting its properties.
const dot = {
radius: 10,
color: 'red',
} satisfies Circle;
// No type error here either 😌
paint(dot.color);After satisfies, the type of dot is:
const dot: {
radius: number;
color: "red";
}"red" satisfies "red" | "blue" | "green", this all passes.
Using Type Guards
We want dot to be a Circle.
We want to paint the dot's color property.
If we make dot a Circle then color becomes Color | undefined.
Personally, I would solve this using type narrowing like:
const dot: Circle = {
radius: 10,
color: 'red',
};
function paint(color: Color) {
console.log(`Painting ${color} paint everywhere`);
}
function paintCircle(circle: Circle) {
if (circle.color === undefined) {
return;
}
paint(circle.color);
}
paintCircle(dot);Now dot is the type we want, and any future Circles (like those coming from function arguments) have a happy path to paint().
