Skip to content

TypeScript types are not accurate when expanding data and it's very annoying. #2327

@chevcast

Description

@chevcast

Describe the bug

I'm creating a new issue under the bug category because I don't think #1556 really captured how irritating this is. It makes using this otherwise fantastic package very tedious.

When you pass the expand property to expand API response data, TypeScript typings are unnaffected, leaving it up to the user to augment the stripe types manually.

To Reproduce

Let's say I want to retrieve the stripe checkout session to simply display a checkout success page. In plain JavaScript it's as simple as this:

const session = await stripe.checkout.sessions.retrieve(checkoutSessionId, {
    expand: ["subscription", "subscription.items.data.product"]
});

Now our session will have the full subscription object and our subscription items will include the associated price AND the product associated with that price.

However, in TypeScript the type of session.subscription is string | Stripe.Subscription | null. Because string is still a possibility I cannot use session.subscription directly. Instead I have to assert the type at every point.

const subscription = session.subscription as Stripe.Subscription | null;

Doesn't seem too bad for a top-level property, but it quickly gets out of hand:

const productName = ((session.subscription as Stripe.Subscription | null)?.items.data[0].price.product as Stripe.Product | null)?.name;

Capturing and asserting the type at every level gets extremely irritating and unreadable so you then end up having to dig deep into the Stripe type tree and create your own expanded types. Still pretty hard to look at, but at least the code is readable again:

type ExpandedSession = Stripe.Checkout.Session & {
    subscription: Stripe.Subscription & {
        items: {
            data: Stripe.SubscriptionItem & {
                price: Stripe.Price & {
                    product: Stripe.Product | null;
                };
            };
        };
    } | null;
};
const session = (await stripe.checkout.sessions.retrieve(checkoutSessionId, {
    expand: ["subscription", "subscription.items.data.product"]
})) as Stripe.Response<ExpandedSession>;

All that work just to eliminate string as a possibility on fields we know we've expanded and can only be the actual object or null. I've never used a package that made managing types this tedious. It's so annoying and so unlike any other well-typed packages I've ever used that I think it deserves to be documented as a bug. Feel free to disagree but I have a hard time believing I'm the only one that finds this such a chore. I shudder to think how many devs are doing as any while using this package 😢

Expected behavior

It is possible to dynamically narrow types within a package based on specified options passed in. I'm no TypeScript expert but I know enough to know that it is indeed possible to narrow the type down to Stripe.Subscription | null, eliminating the string union altogether, based on whether or not the expand option was specified. It's also possible to strongly type the property path strings that you pass to expand based on the other types within the package, making it so a user can only specify actual supported object paths.

While we're at it I'll also point out how unusual it is that we can't just import type { Product } from "stripe" and we are instead forced to reference the types from the top-level Stripe type.

Please bring on a TypeScript guru to give stripe-node a typings overhaul.

Code snippets

OS

Arch

Node version

v22.15.1

Library version

v18.1.0

API version

2025-04-30.basil

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions