Skip to content

Using "satisfies" in TypeScript: A Step-by-Step Example

Updated:

Today, I want to share an interesting example of using the satisfies operator in TypeScript. We will go through the evolution of a code example to see how we can type objects in a safer and more convenient way.

Table of contents

Open Table of contents

Initial Situation

Let’s start with a simple subscription cost calculation function, where we have several plans:

const plans = {
    'basic': (duration: number, cost: number) => duration * cost,
    'premium': (duration: number, cost: number) => duration * cost * 1.5,
    'vip': (duration: number, cost: number) => duration * cost * 2.5
};

const calculate = (plan: string, duration: number, cost: number) => {
    const costFormula = plans[plan];
    return costFormula(duration, cost);
};

const totalCost = calculate('premium', 6, 20);
console.log(totalCost); //180

In this code, plans contains functions, each of which takes duration and cost as parameters and returns the total cost.

However, there is an issue — the calculate function accepts the plan parameter as a string, which is unsafe.

We could pass any value, such as 'gold', and it would cause a runtime error.

Adding Typing for Plans

To avoid errors, we can explicitly specify the types of available subscription plans:

type PlanType = 'basic' | 'premium' | 'vip';

const plans = {
    'basic': (duration: number, cost: number) => duration * cost,
    'premium': (duration: number, cost: number) => duration * cost * 1.5,
    'vip': (duration: number, cost: number) => duration * cost * 2.5
};

const calculate = (plan: PlanType, duration: number, cost: number) => {
    const costFormula = plans[plan];
    return costFormula(duration, cost);
};

const totalCost = calculate('premium', 6, 20);
console.log(totalCost); //180

Now we have restricted the subscription plan type, and TypeScript ensures that only one of the values: 'basic', 'premium', or 'vip' can be passed.

But adding a new plan requires modifying both the PlanType type and the list of plans in the plans object. Let’s fix that.

Using keyof typeof

To simplify, we can use keyof typeof to automatically infer types based on the keys of the plans object:

type PlanType = keyof typeof plans;

const plans = {
    'basic': (duration: number, cost: number) => duration * cost,
    'premium': (duration: number, cost: number) => duration * cost * 1.5,
    'vip': (duration: number, cost: number) => duration * cost * 2.5,
    'ultra': (duration: number, cost: number) => duration * cost * 3.5
};

Now the PlanType type is automatically updated if we add new plans, such as 'ultra'.

Using Record for Typing the Object

We can explicitly type the plans object using Record to ensure type matching between keys and values. This is useful because Record helps explicitly link plan types and functions, making the code more reliable and understandable. Thus, if a new plan is added, it will automatically be checked for type and function compatibility, reducing the likelihood of errors.

type PlanType = keyof typeof plans;

const plans: Record<string, (duration: number, cost: number) => number> = {
    'basic': (duration, cost) => duration * cost,
    'premium': (duration, cost) => duration * cost * 1.5,
    'vip': (duration, cost) => duration * cost * 2.5,
    'ultra': (duration, cost) => duration * cost * 3.5
};

This helps explicitly link plan types and functions, making the code more reliable and understandable. However, using Record<string, ...> makes the type broader since keyof typeof plans becomes string. This means plans can accept any string keys, requiring extra caution when adding new plans. We can revert PlanType to an explicit union type, but that would require manually updating the type each time we add or change plans, which is less convenient.

Using satisfies for Type Checking

Finally, we can use the satisfies operator to let TypeScript check if the plans object matches a specific type while still allowing type inference automatically:

type PlanType = keyof typeof plans;

const plans = {
    basic: (duration, cost) => duration * cost,
    premium: (duration, cost) => duration * cost * 1.5,
    vip: (duration, cost) => duration * cost * 2.5,
    ultra: (duration, cost) => duration * cost * 3.5,
} satisfies Record<string, (duration: number, cost: number) => number>;

const calculate = (plan: PlanType, duration: number, cost: number) => {
    const costFormula = plans[plan];
    return costFormula(duration, cost);
};

const totalCost = calculate('ultra', 6, 20);
console.log(totalCost);

The satisfies operator allows us to check if the object matches the type Record<string, (duration: number, cost: number) => number>, while still keeping type inference so that TypeScript automatically determines all object keys and their types. This makes the code more flexible and error-resistant.

Play

In Simple Words…

In this article, we explored different ways to type a subscription plan object in TypeScript, making the code safer and more reliable. We started with a simple untyped version and gradually added more sophisticated typing strategies: using explicit types, keyof typeof, Record, and finally the satisfies operator. Each step made the code more robust, ensuring that our subscription plans were correctly typed and reducing potential runtime errors. Using satisfies was particularly helpful as it allowed us to keep automatic type inference while ensuring that the object conformed to a specific shape, making our code both flexible and type-safe.


Previous Post
Conditional Object Properties in TypeScript
Next Post
De Morgan's laws