stripe-node
stripe-node copied to clipboard
TypeScript types are not accurate when expanding data and it's very annoying.
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
agreed, and it's shocking coming from a big financial institution to not have their type game in order thank you for writing this up
I would also like to add the fact that it's sometimes difficult to understand which endpoints return auto expanded fields and it ends up with me needing to run the query first and see what's expanded for me or not.
I was just refactoring some Stripe code, and was getting a little annoyed with needing to type out if (typeof x === "string") else { it's the object } everywhere in my code so was just checking in to see if there was any easier way of grabbing the related id for these fields and came across this new ticket.
Just thought I'd share my use case as well. Otherwise quite pleased with Stripe's developer tools lol
Hey @chevcast, thank you for the detailed description!
It totally makes sense that this is a big pain point. We've been discussing internally how types and expandable fields should work and haven't come up with anything conclusive yet.
As you can imagine, it's a complex problem to solve. The recursive and unbounded nature of the expand param means it's hard to express what these really look like at runtime. But, I'm sure there's something we can do that's better than what we have today.
Nothing to share right now, but we'll noodle on it!
As for the architectural frustrations (having to use the top level Stripe.Product), that's also on our radar! We've got the product proposal written to rearchitect everything, it's just currently prioritized behind some more pressing needs. We hope to get to that soon(ish) though.
GraphQL fixes this. Just sayin'.
Hi, I did a PR to implement this very specific feature on the Spotify SDK a few years ago.
https://github.com/spotify/spotify-web-api-ts-sdk/commit/ea9f5288d1303d54103a59e18b42aca38f102873
Feel free to reuse this work to achieve the desired output 😄