Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: improve typing and logic for filterTruthy and filterFalsy operators #53

Merged
merged 1 commit into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 65 additions & 10 deletions packages/rxjs/src/operators/filter-falsy.operator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,74 @@
import { filterFalsy } from '@bimeister/utilities.rxjs';
import { from, Observable } from 'rxjs';
import { filterFalsy } from './filter-falsy.operator';

describe('filter-falsy.operator.ts', () => {
it('should filter truth value', () => {
const input$: Observable<unknown> = from([1, 'string', false]);
it('should filter out truthy values', () => {
const input$: Observable<unknown> = from([1, 'string', false, null, undefined, 0, true]);

const emits: unknown[] = [];

input$
.pipe(filterFalsy())
.subscribe((output: unknown): void => {
emits.push(output);
})
.unsubscribe();
input$.pipe(filterFalsy()).subscribe((output: unknown): void => {
emits.push(output);
});

expect(emits).toEqual([false]);
expect(emits).toEqual([false, null, undefined, 0]);
});

it('should correctly filter out truthy numbers', () => {
const input$: Observable<number | null | undefined> = from([0, 1, 2, 3, null, undefined]);

const emits: (number | null | undefined)[] = [];

input$.pipe(filterFalsy()).subscribe((output: number | null | undefined): void => {
emits.push(output);
});

expect(emits).toEqual([0, null, undefined]);
});

it('should correctly filter out truthy strings', () => {
const input$: Observable<string | null | undefined> = from(['', 'hello', null, 'world', undefined]);

const emits: (string | null | undefined)[] = [];

input$.pipe(filterFalsy()).subscribe((output: string | null | undefined): void => {
emits.push(output);
});

expect(emits).toEqual(['', null, undefined]);
});

it('should filter out truthy objects', () => {
interface Item {
id: number;
name: string;
}

const input$: Observable<Item | null | undefined> = from([
{ id: 1, name: 'Item 1' },
null,
{ id: 2, name: 'Item 2' },
undefined
]);

const emits: (null | undefined)[] = [];

input$.pipe(filterFalsy()).subscribe((output: null | undefined): void => {
emits.push(output);
});

expect(emits).toEqual([null, undefined]);
});

it('should filter out `NaN` as a falsy value', () => {
const input$: Observable<number> = from([NaN, 1, 0, 3, NaN]);

const emits: number[] = [];

input$.pipe(filterFalsy()).subscribe((output: number): void => {
emits.push(output);
});

expect(emits).toEqual([NaN, 0, NaN]);
});
});
24 changes: 13 additions & 11 deletions packages/rxjs/src/operators/filter-falsy.operator.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import type { MonoTypeOperatorFunction, Observable } from 'rxjs';
import type { Falsy } from '@bimeister/utilities.types';
import type { Observable, OperatorFunction } from 'rxjs';
import { filter } from 'rxjs/operators';

/**
* Filters out truthy values from the source observable.
* Filters out truthy values from the source observable, emitting only falsy values
* (`false`, `0`, `''`, `null`, `undefined`, `NaN`).
*
* @template T - The type of elements emitted by the source observable.
* @returns - An operator that filters out truthy values from the source observable.
* @returns An operator that filters out truthy values from the source observable.
* @example
* const input$: Observable<unknown> = from([1, 'string', false, true]);

input$
.pipe(filterFalsy())
.subscribe((output: unknown) => { ... })
* const input$: Observable<unknown> = from([1, 'string', false, null, undefined, 0]);
*
* input$
* .pipe(filterFalsy())
* .subscribe((output: unknown) => { ... });
*/
export const filterFalsy: <T>() => MonoTypeOperatorFunction<T> =
export const filterFalsy: <T>() => OperatorFunction<T, Extract<T, Falsy>> =
<T>() =>
(source: Observable<T>): Observable<T> =>
source.pipe(filter<T>((value: T) => !Boolean(value)));
(source: Observable<T>): Observable<Extract<T, Falsy>> =>
source.pipe(filter((value: T): value is Extract<T, Falsy> => !Boolean(value)));
80 changes: 69 additions & 11 deletions packages/rxjs/src/operators/filter-truthy.operator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,77 @@
import { from, Observable } from 'rxjs';
import { filterTruthy } from '@bimeister/utilities.rxjs';
import { filterTruthy } from './filter-truthy.operator';

describe('filter-truth.operator.ts', () => {
it('should filter falsy value', () => {
const input$: Observable<unknown> = from([1, 'string', false]);
describe('filter-truthy.operator.ts', () => {
it('should filter out falsy values', () => {
const input$: Observable<unknown> = from([1, 'string', false, 0, null, undefined, true]);

const emits: unknown[] = [];

input$
.pipe(filterTruthy())
.subscribe((output: unknown): void => {
emits.push(output);
})
.unsubscribe();
input$.pipe(filterTruthy()).subscribe((output: unknown): void => {
emits.push(output);
});

expect(emits).toEqual([1, 'string']);
expect(emits).toEqual([1, 'string', true]);
});

it('should correctly handle numbers, filtering out falsy ones', () => {
const input$: Observable<number | null | undefined> = from([0, 1, 2, 3, null, undefined]);

const emits: number[] = [];

input$.pipe(filterTruthy()).subscribe((output: number): void => {
emits.push(output);
});

expect(emits).toEqual([1, 2, 3]);
});

it('should work with strings, filtering out empty strings', () => {
const input$: Observable<string | null | undefined> = from(['', 'hello', null, 'world', undefined]);

const emits: string[] = [];

input$.pipe(filterTruthy()).subscribe((output: string): void => {
emits.push(output);
});

expect(emits).toEqual(['hello', 'world']);
});

it('should filter out null and undefined values', () => {
const input$: Observable<string | null | undefined> = from([null, undefined, 'defined']);

const emits: string[] = [];

input$.pipe(filterTruthy()).subscribe((output: string): void => {
emits.push(output);
});

expect(emits).toEqual(['defined']);
});

it('should work with complex objects, filtering out falsy ones', () => {
interface Item {
id: number;
name: string;
}

const input$: Observable<Item | null | undefined> = from([
{ id: 1, name: 'Item 1' },
null,
{ id: 2, name: 'Item 2' },
undefined
]);

const emits: Item[] = [];

input$.pipe(filterTruthy()).subscribe((output: Item): void => {
emits.push(output);
});

expect(emits).toEqual([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
]);
});
});
22 changes: 11 additions & 11 deletions packages/rxjs/src/operators/filter-truthy.operator.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import type { MonoTypeOperatorFunction, Observable } from 'rxjs';
import type { Observable, OperatorFunction } from 'rxjs';
import { filter } from 'rxjs/operators';

/**
* Filters out falsy values from the source observable.
* Filters out falsy values (`false`, `0`, `''`, `null`, `undefined`, `NaN`) from the source observable.
*
* @template T - The type of elements emitted by the source observable.
* @returns - An operator that filters out falsy values from the source observable.
* @returns An operator that filters out falsy values from the source observable.
* @example
* const input$: Observable<unknown> = from([1, 'string', false, true]);

input$
.pipe(filterTruthy())
.subscribe((output: unknown) => { ... })
* const input$: Observable<unknown> = from([1, 'string', false, true]);
*
* input$
* .pipe(filterTruthy())
* .subscribe((output: unknown) => { ... });
*/
export const filterTruthy: <T>() => MonoTypeOperatorFunction<T> =
export const filterTruthy: <T>() => OperatorFunction<T, NonNullable<T>> =
<T>() =>
(source: Observable<T>): Observable<T> =>
source.pipe(filter<T>((value: T) => Boolean(value)));
(source: Observable<T>): Observable<NonNullable<T>> =>
source.pipe(filter((value: T): value is NonNullable<T> => Boolean(value)));
40 changes: 21 additions & 19 deletions packages/rxjs/src/operators/tap-on-instance-of.operator.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
import type { Constructor } from 'packages/types/src/constructor.type';
import type { Observable, OperatorFunction } from 'rxjs';
import type { MonoTypeOperatorFunction, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

/**
* Conditionally applies a callback using the tap operator based on the instance of specified types.
*
* @template I - The type of instances to check for in the source observable.
* @param typeOrTypes - The type or types to check for instance of in the source observable.
* @param callback - The callback function to be invoked when an instance of the specified type is found.
* @returns - An operator that performs a side effect for each emission on the source observable.
* @template T - The type of values emitted by the source observable.
* @template U - The type of values that are checked for instances in the source observable.
* @param typeOrTypes - The type or types to check for instances in the source observable.
* @param callback - The callback function to be invoked when an instance of the specified type(s) is found.
* @returns - An operator that performs a side effect only for the emissions of the specified instance type(s).
* @example
* Based on the values produced by the source observable,
* performs a side-effect only on an instance of the specified SomeClass type
* const input$: Observable<unknown> = from([1, 'string', { name: 'Some name' }, new SomeClass()]);

input$
.pipe(tapOnInstanceOf(SomeClass, () => this.showAlert()))
* Example with a single type
* const input$: Observable<unknown> = from([1, 'string', new SomeClass()]);
* input$.pipe(tapOnInstanceOf(SomeClass, (instance) => console.log('Found instance:', instance)));
*
* Example with multiple types
* const input$: Observable<unknown> = from([1, 'string', new SomeClassA(), new SomeClassB()]);
* input$.pipe(tapOnInstanceOf([SomeClassA, SomeClassB], (instance) => console.log('Found instance:', instance)));
*/
export function tapOnInstanceOf<T>(
typeOrTypes: Constructor<T> | Constructor<T>[],
callback: (value: T) => void
): OperatorFunction<unknown, unknown> {
return (source$: Observable<unknown>) =>
export function tapOnInstanceOf<T, U>(
typeOrTypes: Constructor<U> | Constructor<U>[],
callback: (value: U) => void
): MonoTypeOperatorFunction<T> {
return (source$: Observable<T>) =>
source$.pipe(
tap((value: unknown) => {
const types: Constructor<T>[] = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];
tap((value: T) => {
const types: Constructor<U>[] = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];

types.forEach((type: Constructor<T>) => {
types.forEach((type: Constructor<U>) => {
if (value instanceof type) {
callback(value);
}
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/falsy.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Falsy = false | 0 | '' | null | undefined | typeof NaN;