const rr = this.getSubscription$(subscriptionId).p...

Criado em: 12 de julho de 2025

Respondido usando O3 por Chat01

Pergunta

const rr = this.getSubscription$(subscriptionId).pipe(
switchMap(result => {
if (result.result === Result.ok) {
return of(result);
}

text
console.error(`creditCardPaymentStageOne: getSubscription #1 failed!`); return throwError( () => new ServiceResult<{ redirectUrl: string }>({ result: Result.retryPayment_cc_firstSubscriptionRetrievalFailed, message: GENERIC_PAYMENT_ERROR, }) ); }), switchMap(result => { const subscription = result.data; subscription.BillingAddress = billingAddress; subscription.ShippingAddress = shippingAddress; const creditCardId = subscription.CreditCardId ?? subscription.CreditCard?.Id ?? 0; creditCard.Id = creditCardId || 0; subscription.CreditCard = creditCard; return this.updateSubscription$(subscription); }), switchMap(result => { if (result.result === Result.ok) { console.log(`processPaymentWithStripeCheckout: updateSubscription completed successfully!`); // we've received an updated copy of the subscription so we should update it in the data store this.updateSubscriptionInStore(result.data); this.wireTransferDetailsUpdatedSubject$.next({ subscriptionId: result.data.Id }); return of(result); } console.error(`processPaymentWithStripeCheckout: updateSubscription failed!`); return throwError( () => new ServiceResult<{ redirectUrl: string }>({ result: Result.retryPayment_cc_subscriptionUpgradingFailed, message: GENERIC_PAYMENT_ERROR, }) ); }), switchMap(() => { const getSubscriptionTransactions$ = this.usersService.isUserReseller$$() ? super.getSubscriptionTransactions(subscriptionId) : super.getSubscriptionTransactionsForCurrentCustomer(subscriptionId); const uoieur = getSubscriptionTransactions$.pipe( map(transactions => transactions.filter( transaction => transaction.GatewayStatus === TransactionPaymentStatus.Pending && transaction.TransactionStatus === TransactionStatus.Successful && transaction.GatewayTransactionCheckoutSessionUrl ) ), map(transactions => { if (transactions.length === 0) { return new ServiceResult<{ redirectUrl: string }>({ result: Result.retryPayment_stripe_transactionNotFound, message: GENERIC_PAYMENT_ERROR, }); } // sort the transactions list by date (descending) transactions.sort( (a, b) => // a - b for ascending sorting // b - a for descending sorting new Date(b.TransactionDate).getTime() - new Date(a.TransactionDate).getTime() ); return new ServiceResult<{ redirectUrl: string }>({ result: Result.ok, data: { redirectUrl: transactions[0].GatewayTransactionCheckoutSessionUrl }, }); }), catchError(err => { console.error('processPaymentWithStripeCheckout: getSubscriptionTransactions failed!', err); return of( new ServiceResult<{ redirectUrl: string }>({ result: Result.retryPayment_stripe_transactionsRetrievalFailed, message: GENERIC_PAYMENT_ERROR, }) ); }) ); return uoieur; }), catchError((err: ServiceResult<{ redirectUrl: string }>) => of(err)) );

Why is type of rr inferred as Observable<ServiceResult<{
redirectUrl: string;
}> | ServiceResult<{
redirectUrl: string;
}> | ServiceResult<{
redirectUrl: string;
}>> ? Why not Observable<ServiceResult<{redirectUrl: string;}>> ?

Pensamento

Analyzing observable return types

Okay, the type should eventually be Observable<ServiceResult<...>>. I wonder, though, if the union includes a plain ServiceResult because one of the switchMap callbacks returns ServiceResult directly (not wrapped in an Observable). In the third switchMap, it returns an Observable. However, initially, there’s a catchError which returns Observable<ServiceResult>, but the err param is typed as ServiceResult. So, it all still should be Observable. Let’s check the first two switchMap steps to confirm.

Clarifying observable return types

I'm thinking through the sequence of switchMap operations. The first one checks if result.result === Result.ok, in which case it returns of(result) — that's an Observable. Similarly, the second switchMap updates the subscription and returns Observable<ServiceResult>. The third switches to of(result) or throws, also returning an Observable. I wonder if this.updateSubscription$ possibly returns a ServiceResult instead of an Observable. Maybe TypeScript's type inference is causing an issue, leading to the union with ServiceResult. Let's try replicating this in TypeScript.

Analyzing Union Type Behavior

It looks like the issue comes from how TypeScript handles unions, specifically when a union of Observable<ServiceResult> and ServiceResult is produced inside switchMap. The catchError function can also return an Observable<ServiceResult>, which might contribute to the duplication in the union type. This happens because TypeScript doesn't always deduplicate equivalent types in unions, especially when generics differ. I'm trying to replicate this behavior in a simplified example to better understand why the union duplicates. It seems like the type system treats structurally identical types as distinct, which could be causing the problem.

Understanding Union Duplicates in TypeScript

When using catchError in RxJS, the resulting type is a union of the original observable's type and the recovery observable's type. Even if both types are exactly the same (e.g., ServiceResult<{ redirectUrl: string }>), TypeScript doesn't collapse these duplicate types in the union automatically. This leads to repeated types in the union, like Observable<ServiceResult | ServiceResult | ServiceResult>, even though semantically they are the same. It's due to TypeScript's type inference and aliasing when generics are used with this operator.

Thought for 1m 35s

Resposta

catchError (and, to a lesser extent, switchMap) is what makes the compiler build that “ugly” type.

1 What the operator’s signature looks like

ts
// rxjs/operators/catchError.d.ts (simplified) catchError<T, O>( // O = what you return in the selector selector: (err: any) => ObservableInput<O> ): OperatorFunction<T, T | O>; // output is *T OR O*
  • T is the type that was flowing through the stream before the catchError.
  • Whatever you return inside the selector becomes O.
  • The operator always widens the output to T | O.

2 How that plays out in your chain

  • Before the last catchError the stream carries
    ServiceResult<{ redirectUrl: string }> (call that T).
  • Inside the selector you do of(err) where err has the exact same type, so the compiler infers O = ServiceResult<{ redirectUrl: string }> as well.

The operator therefore spits out T | O, i.e.

ServiceResult<{ redirectUrl: string }> | ServiceResult<{ redirectUrl: string }>

You used catchError (directly or inside a nested pipe) three times, so the same rule was applied three times. The pretty-printer doesn’t bother to collapse identical union members, hence the verbose:

text
Observable< ServiceResult<{ redirectUrl: string; }> | ServiceResult<{ redirectUrl: string; }> | ServiceResult<{ redirectUrl: string; }> >

Semantically it’s still just Observable<ServiceResult<{ redirectUrl: string }>>; the duplicates don’t change anything.

3 How to make the annotation look “clean”

Tell catchError explicitly that the recovery type is the same as the incoming one:

ts
catchError< ServiceResult<{ redirectUrl: string }>, // T ServiceResult<{ redirectUrl: string }> // O >(err => of(err))

—or cast inside the selector:

ts
catchError(err => of(err as ServiceResult<{ redirectUrl: string }>))

Either way, the whole expression is now inferred (and displayed) as:

ts
Observable<ServiceResult<{ redirectUrl: string }>>

The duplication is purely a quirk of TypeScript’s overload inference and how it prints unions; your stream’s runtime behaviour is already correct.

Pergunta

is there a way of making this observable chain more elegant and easy to understand?

Pensamento

Improving RxJS Chain

To make the user's RxJS observable chain more elegant and understandable, I’d suggest a few things:

  • Use pipeRxjs or similar patterns.
  • Split the flow into smaller, named custom operators or helper functions.
  • Refactor switchMap operators into separate functions (e.g., getSubscription, fetchRedirectUrl).
  • Consider exhaustMap where appropriate.
  • Flatten the chain with concatMap or switchMap inside higher-order functions.
  • Use finalize for side-effects and tap when operation is independent of the observable response.

Improving RxJS Chain Elegance

To make the user's chain more elegant, I suggest refactoring with small, pure functions. One way is defining helpers like okOrThrow to simplify error handling, and using RxJS's iif for conditional Observable selection. We can introduce custom operators, like ensureOk, to handle ServiceResult mapping more cleanly. Additionally, refactor side effects using tap, and consider simplifying types or unifying them to avoid duplication. These changes make the chain clearer and more manageable without losing functionality.

Thought for 25 seconds

Resposta

Below are some ideas (and a sketch of what the code could look like) that usually make long RxJS pipelines easier to read, test and maintain. Pick only what feels natural for your team’s style guide.


1 Factor-out business helpers first

Put any “business” decisions in tiny pure helpers and keep the pipe focused on when things happen:

ts
/** Throw a typed ServiceResult if the result isn’t ok */ const ensureOk = <T>(code: Result) => (sr: ServiceResult<T>): Observable<T> => sr.result === Result.ok ? of(sr.data) // happy path : throwError(() => new ServiceResult<T>({ result: code, message: GENERIC_PAYMENT_ERROR }) ); /** Add/patch addresses and card fields */ const patchSubscription = (billing: Address, shipping: Address, cc: CreditCard) => (s: Subscription): Subscription => ({ ...s, BillingAddress: billing, ShippingAddress: shipping, CreditCard: { ...cc, Id: s.CreditCardId ?? s.CreditCard?.Id ?? 0 }, }); /** Choose the right transaction source for the current user */ const loadTransactions = (id: number) => this.usersService.isUserReseller$$() ? super.getSubscriptionTransactions(id) : super.getSubscriptionTransactionsForCurrentCustomer(id);

2 Use tap/​finalize for side-effects

Side-effects (logging, store updates, Subjects, etc.) are clearer when they’re isolated in tap/finalize, not mixed in switchMap.


3 Collapse repeated catchError into one

Handle every thrown ServiceResult once at the very end; you avoid the growing union type and you keep the pipeline linear.
If you still need local recovery, wrap it in its own operator (e.g. ensureOk above) so that only one catchError remains in the outer chain. That keeps the inferred type a clean Observable<ServiceResult<{redirectUrl: string}>> Angular University Intertech.


4 Put the whole flow together

ts
const rr$ = this.getSubscription$(subscriptionId).pipe( switchMap(ensureOk<Subscription>(Result.retryPayment_cc_firstSubscriptionRetrievalFailed)), map(patchSubscription(billingAddress, shippingAddress, creditCard)), switchMap(sub => this.updateSubscription$(sub)), tap(({ data }) => { this.updateSubscriptionInStore(data); this.wireTransferDetailsUpdatedSubject$.next({ subscriptionId: data.Id }); }), switchMap(ensureOk<void>(Result.retryPayment_cc_subscriptionUpgradingFailed)), switchMap(() => loadTransactions(subscriptionId)), map(txns => txns .filter( t => t.GatewayStatus === TransactionPaymentStatus.Pending && t.TransactionStatus === TransactionStatus.Successful && t.GatewayTransactionCheckoutSessionUrl ) .sort( (a, b) => new Date(b.TransactionDate).getTime() - new Date(a.TransactionDate).getTime() ) ), map(sorted => { if (!sorted.length) { throw new ServiceResult<{ redirectUrl: string }>({ result: Result.retryPayment_stripe_transactionNotFound, message: GENERIC_PAYMENT_ERROR, }); } return new ServiceResult<{ redirectUrl: string }>({ result: Result.ok, data: { redirectUrl: sorted[0].GatewayTransactionCheckoutSessionUrl }, }); }), catchError(err => of(err)) // single place );

What changed

BeforeAfter
Three nested catchError blocksOne final catchError
Repeated inline checksRe-usable ensureOk helper
SwitchMaps with logging & store updatesPure transforms + tap for side-effects
Verbose union typeConsistently Observable<ServiceResult<…>> (no duplicate members)

5 Optional syntactic sugar

If the ServiceResult pattern is common, wrap it in a custom operator so your pipes read like prose:

ts
source$.pipe( assertOk(Result.someFailure), // … )

Documentation pages on RxJS error handling and operator placement back up these patterns 0search6.


Bottom line

By extracting what happens into helpers and keeping the observable chain a straight list of operators with just one place that “catches everything,” the flow becomes self-documenting and TypeScript’s hinted type collapses to the single, readable form you expected.

Pergunta

public updateSubscription(subscription: Subscription) { const updateSubscription = this.usersService.isUserReseller
? super.updateSubscription(this.site.ReferenceId, subscription)
: super.updateSubscriptionForCurrentCustomer(subscription);

text
return updateSubscription$.pipe( map( updatedSubscription => new ServiceResult<Subscription>({ result: Result.ok, data: updatedSubscription, }) ), catchError(err => { console.error(`${ID}: updating subscription #${subscription.Id} failed!`, err); return of( new ServiceResult<Subscription>({ result: Result.subscriptionUpdatingFailed, }) ); }) );

}

The above method returns Observable<ServiceResult<Subscription>>

#processPaymentWithStripeCheckoutStep3(subscriptionId: number) { const getSubscriptionTransactions = this.usersService.isUserReseller$$()
? super.getSubscriptionTransactions(subscriptionId)
: super.getSubscriptionTransactionsForCurrentCustomer(subscriptionId);

text
return getSubscriptionTransactions$.pipe( map(transactions => transactions.filter( transaction => transaction.GatewayStatus === TransactionPaymentStatus.Pending && transaction.TransactionStatus === TransactionStatus.Successful && transaction.GatewayTransactionCheckoutSessionUrl ) ), map(transactions => { if (transactions.length === 0) { return new ServiceResult<{ redirectUrl: string }>({ result: Result.retryPayment_stripe_transactionNotFound, message: 'GENERIC_PAYMENT_ERROR', }); } // sort the transactions list by date (descending) transactions.sort( (a, b) => // a - b for ascending sorting // b - a for descending sorting new Date(b.TransactionDate).getTime() - new Date(a.TransactionDate).getTime() ); return new ServiceResult<{ redirectUrl: string }>({ result: Result.ok, data: { redirectUrl: transactions[0].GatewayTransactionCheckoutSessionUrl }, }); }), catchError(err => { console.log('processPaymentWithStripeCheckoutStep3: getSubscriptionTransactions failed!', err); return of( new ServiceResult<{ redirectUrl: string }>({ result: Result.retryPayment_stripe_transactionsRetrievalFailed, message: 'GENERIC_PAYMENT_ERROR', }) ); }) );

}

This other method returns Observable<ServiceResult<{redirectUrl: string;}> | ServiceResult<{redirectUrl: string;}>> yet they are implemented in the same way ... Why is this second method producing a union while first one does not?

Pensamento

Analyzing union duplicates

It seems like union duplicates may arise because of distinct object literal types, even though they appear identical. TypeScript treats object literals as fresh, distinct types each time they are defined. When we use them in generics, it may not collapse duplicates due to this type identity distinction. However, in cases like updateSubscription$, TypeScript collapses duplicates because the generic parameters are type aliases, referencing the same symbol. So, the difference in behavior could be due to how generics and object literals interact with TypeScript’s identity system.

Explaining union duplicates in TypeScript

The union duplicates appear because TypeScript treats each inline object literal like { redirectUrl: string } as a new, un-named type. This prevents duplicates from collapsing, as TypeScript only deduplicates union members that have a completely identical type symbol. To avoid this, using a named type or interface, like interface Redirect { redirectUrl: string }, helps collapse the duplicates. In the case of updateSubscription$, the named Subscription type allows for collapse, while the anonymous object literal in the other example keeps them distinct.

Explaining union duplicates and catchError behavior

In TypeScript, each object literal type is treated as "fresh," which prevents union types from collapsing unless they’re exactly the same. But inside generics, especially with catchError in an observable pipeline, duplicates can accumulate. If there are multiple catchError calls, the total count of duplicates increases as each call may produce a union. This explains the difference between methods: one has a single catchError, collapsing duplicates, while the other has more, keeping them distinct. The key is understanding how generics and error handling interact.

Thought for 2m 19s

Resposta

catchError is the reason both pipelines get widened, but the kind of type you give to ServiceResult decides whether the compiler can collapse the union or has to keep both members.


1 Why catchError always produces T | O

ts
// simplified rxjs typing catchError<T, O>(selector: (err: any) => ObservableInput<O>) : OperatorFunction<T, T | O>;
  • T – the element type that was flowing through the stream before the operator.
  • O – the element type you emit from the selector (of(…), throwError(…), …).
  • The output is therefore T | O, i.e. the old type or the recovery type.

So every catchError adds one layer of “… | …”.


2 Why the first method stays clean

ts
map(new ServiceResult<Subscription>() ), // <-- T catchError(err => of(new ServiceResult<Subscription>()) // <-- O )

Here T and O are the very same named type ServiceResult<Subscription>:

  • Because they refer to a single, shared symbol (Subscription), TypeScript can de-duplicate the union, giving you
ts
Observable<ServiceResult<Subscription>>

3 Why the second method shows a duplicate

ts
map(new ServiceResult<{ redirectUrl: string }>() ), // <-- T catchError(err => of(new ServiceResult<{ redirectUrl: string }>()) // <-- O )

Each time you write an inline object literal type ({ redirectUrl: string }) inside a type argument, TypeScript creates a fresh, anonymous type.
They are structurally identical, but they’re not the same type symbol, so T and O are viewed as different:

ts
Observable< ServiceResult<{ redirectUrl: string }> | ServiceResult<{ redirectUrl: string }> >

(The IDE prints them, even though at runtime they behave the same.)


4 How to collapse the union

Give the payload a name (or tell catchError its generics explicitly):

ts
type RedirectPayload = { redirectUrl: string }; return getSubscriptionTransactions$.pipe( map(txns => new ServiceResult<RedirectPayload>({ result: Result.ok, data: { redirectUrl:} }) ), catchError(err => of(new ServiceResult<RedirectPayload>({ result: Result.retryPayment_stripe_transactionsRetrievalFailed, message: GENERIC_PAYMENT_ERROR, })) ) ); // → Observable<ServiceResult<RedirectPayload>>

or

ts
catchError< ServiceResult<{ redirectUrl: string }>, ServiceResult<{ redirectUrl: string }> >(err => of(err));

Either approach lets TypeScript see a single concrete type, so the verbose A | A union disappears.

Compartilhar esta Q&A