To use short paths, to easy refactor, reduces the file path changes.
β Good
// module/logic.ts
export const someLogic = () => { ...};
// module/index.ts
export * from './logic';
// consumer.ts
import { someLogic } from './module';
β Bad
// consumer.ts
import { someLogic } from './module/logic';
Avoid primitive obsession.
β Good
type UserId = Nominal<'user.userId', number>;
type RoleId = Nominal<'user.roleId', number>;
function foo(userId: UserId, roleId: RoleId) {}
β Bad
function foo(userId: number, roleId: number) {}
- Define a nominal type using the
Nominal
helper.
type UserId = Nominal<'user.userId', number>;
- Define a parser with validation and so on.
const UserId = {
parse(x: unknown) {
// Validate x...
return x as UserId;
},
} as const;
- Use in the consumer's code
import { UserId } from './user-id'
const userId = UserId.parse(1);
function foo(userId: UserId) {}
foo(userId);
- Allows to cast to the underlying type.
const n: number = userId; // Ok it refers to a number (kind of Covariance),
const userId: UserId = 1; // Error but not vice versa
- Protects from passing invalid values.
const userId = UserId.parse(1);
const roleId = RoleId.parse(100);
function foo(userId: UserId, roleId: RoleId) {}
foo(userId, roleId); // Ok
foo(roleId, userId); // Error
foo(-1, 0); // Error
- Zero-cost abstraction.
// compiled js:
const roleId = 100; // After compilation it's just value
Combine hasError
with formGroup.touched
, It increases UX, reduces the 'noise' in the form.
Don't use [disabled]="formGroup.invalid"
it will confuse the user there isn't a clue to enable the button.
<mat-form-field>
<mat-label>Name</mat-label>
<input type="text" matInput [formControl]="nameControl" >
<mat-error *ngIf="nameControl.hasError('required') && formGroup.touched">
Name is <strong>required</strong>
</mat-error>
</mat-form-field>
<button mat-button (click)="onClick">Submit</button>
Errors will be displayed after user pressed to the one.
public onClick() {
this.formGroup.markAsTouched();
if (this.formGroup.invalid) {
return;
}
// ...
}
const items$ = this.httpClient.get<T>(...).pipe(
startWith(pending()),
) // Observable<T | Pending>
The stream will emit Pending
while T is loading. It's useful to display a progress bar or placeholder.
So, items$
has one of states: Pending
or T
. You might want to unwrap items$
to get only T
:
<ng-container *wexUnwrap="items$ | async as items">
<h3>Pending container</h3>
{{ items }} <!-- T | null -->
</ng-container>
use state.pending
to display progress state during loading:
<ng-container *wexUnwrap="items$ | async as items; state as s">
<h3>Pending container</h3>
<div *ngIf="s.pending">Loading...</div>
{{ items }} <!-- T | null -->
</ng-container>
or with template:
<ng-container *wexUnwrap="items$ | async as items; pending: pending">
<h3>Pending container</h3>
{{ items }}
</ng-container>
<ng-template #pending> Loading... </ng-template>
The whole content will be replaced with #pending
template.
The Rxjs streams are stopped once an error has occurred in. You might want to handle it by the catchError
operator.
Example:
A http call might be completed with an error. To handle, add catchCoreError
to the pipe before startWith
.
const items$ = this.httpClient.get<T>(...).pipe(
catchCoreError(),
startWith(pending()),
) // Observable<T | Pending | CoreResultError>
Use the same *wexUnwrap
to get T
:
<ng-container *wexUnwrap="items$ | async as items;">
<h3>Pending container</h3>
{{ items }} <!-- T | null -->
</ng-container>
*wexUnwrap
handles both CoreResultError
and Pending
distinguishing T from.
use state
to display progress and error states:
<ng-container *wexUnwrap="items$ | async as items; state as s">
<h3>Pending container</h3>
<div *ngIf="s.pending">Loading...</div>
<div *ngIf="s.error">Error.</div>
{{ items }} <!-- T | null -->
</ng-container>
or with templates:
<ng-container *wexUnwrap="items$ | async as items; pending: pending; error: error">
<div>Pending container</div>
{{ items }}
</ng-container>
<ng-template #pending> Loading... </ng-template>
<ng-template #error> Error occurred. </ng-template>
If you need to retry operation and application to be resilient after an error occurred, use
catchCoreError
Otherwise, the error will be caught by the global error handler and some functionality might stop working and restored only after refreshing the page.
The wrap
is already encapsulating those technics:
return this.httpClient.http
.get<T>(...) // Observable<T>
.pipe(wrap()); // Observable<T | CoreResultError | Pending>
Under the hood it looks:
function wrap<T>(): OperatorFunction<T, CoreResult<T>> {
return pipe(catchCoreError(), startWith(pending()));
}
type CoreResult<T> = T | CoreResultError | Pending;
type CoreResultError =
| HttpResponseError
| Error
// and other types
;
The non-cancelable version of wrap. It could be useful for side effects, like a entity creating.
wrapAsync(() => this.http.post<T>(...).toPromise()) // Observable<T | CoreResultError | Pending>
Under the hood:
function wrapAsync<TResult>(builder: () => Promise<TResult>): Observable<CoreResult<TResult>> {
return defer(() => builder())
.pipe(
catchCoreError(),
startWith(pending())
);
}