const rr = this.getSubscription$(subscriptionId).p...
作成日: 2025年7月12日
作成日: 2025年7月12日
const rr = this.getSubscription$(subscriptionId).pipe(
switchMap(result => {
if (result.result === Result.ok) {
return of(result);
}
textconsole.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;}>> ?
catchError
(and, to a lesser extent, switchMap
) is what makes the compiler build that “ugly” type.
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
.O
.T | O
.catchError
the stream carriesServiceResult<{ redirectUrl: string }>
(call that T).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:
textObservable< ServiceResult<{ redirectUrl: string; }> | ServiceResult<{ redirectUrl: string; }> | ServiceResult<{ redirectUrl: string; }> >
Semantically it’s still just Observable<ServiceResult<{ redirectUrl: string }>>
; the duplicates don’t change anything.
Tell catchError
explicitly that the recovery type is the same as the incoming one:
tscatchError< ServiceResult<{ redirectUrl: string }>, // T ServiceResult<{ redirectUrl: string }> // O >(err => of(err))
—or cast inside the selector:
tscatchError(err => of(err as ServiceResult<{ redirectUrl: string }>))
Either way, the whole expression is now inferred (and displayed) as:
tsObservable<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.
is there a way of making this observable chain more elegant and easy to understand?
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.
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);
Side-effects (logging, store updates, Subjects, etc.) are clearer when they’re isolated in tap
/finalize
, not mixed in switchMap
.
catchError
into oneHandle 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.
tsconst 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
Before | After |
---|---|
Three nested catchError blocks | One final catchError |
Repeated inline checks | Re-usable ensureOk helper |
SwitchMaps with logging & store updates | Pure transforms + tap for side-effects |
Verbose union type | Consistently Observable<ServiceResult<…>> (no duplicate members) |
If the ServiceResult pattern is common, wrap it in a custom operator so your pipes read like prose:
tssource$.pipe( assertOk(Result.someFailure), // … )
Documentation pages on RxJS error handling and operator placement back up these patterns 0search6.
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.
public updateSubscription(subscription: Subscription) {
const updateSubscription = this.usersService.isUserReseller
? super.updateSubscription(this.site.ReferenceId, subscription)
: super.updateSubscriptionForCurrentCustomer(subscription);
textreturn 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);
textreturn 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?
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.
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(…)
, …).T | O
, i.e. the old type or the recovery type.So every catchError
adds one layer of “… | …
”.
tsmap( … new ServiceResult<Subscription>(…) ), // <-- T catchError(err => of(new ServiceResult<Subscription>( … )) // <-- O )
Here T and O are the very same named type ServiceResult<Subscription>
:
Subscription
), TypeScript can
de-duplicate the union, giving youtsObservable<ServiceResult<Subscription>>
tsmap( … 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:
tsObservable< ServiceResult<{ redirectUrl: string }> | ServiceResult<{ redirectUrl: string }> >
(The IDE prints them, even though at runtime they behave the same.)
Give the payload a name (or tell catchError
its generics explicitly):
tstype 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
tscatchError< 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.