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

fix(iif): No longer allow accidental undefined arguments #5829

Merged
merged 1 commit into from
Oct 20, 2020
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
4 changes: 3 additions & 1 deletion api_guard/dist/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,9 @@ export declare type Head<X extends any[]> = ((...args: X) => any) extends (arg:

export declare function identity<T>(x: T): T;

export declare function iif<T = never, F = never>(condition: () => boolean, trueResult?: ObservableInput<T>, falseResult?: ObservableInput<F>): Observable<T | F>;
export declare function iif<T>(condition: () => true, trueResult: ObservableInput<T>, falseResult: ObservableInput<any>): Observable<T>;
export declare function iif<F>(condition: () => false, trueResult: ObservableInput<any>, falseResult: ObservableInput<F>): Observable<F>;
export declare function iif<T, F>(condition: () => boolean, trueResult: ObservableInput<T>, falseResult: ObservableInput<F>): Observable<T | F>;

export interface InteropObservable<T> {
[Symbol.observable]: () => Subscribable<T>;
Expand Down
28 changes: 18 additions & 10 deletions spec-dtslint/observables/iif-spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { iif, of } from 'rxjs';
import { a$, b$ } from 'helpers';
import { iif, EMPTY } from 'rxjs';

it('should accept function as first parameter', () => {
const a = iif(() => false); // $ExpectType Observable<never>
});
const randomBoolean = () => Math.random() > 0.5;

it('should infer correctly with 2 parameters', () => {
const a = iif(() => false, of(1)); // $ExpectType Observable<number>
it('should error for insufficient parameters', () => {
const r0 = iif(randomBoolean); // $ExpectError
const r1 = iif(randomBoolean, a$); // $ExpectError
const r2 = iif(randomBoolean, undefined, b$); // $ExpectError
});

it('should infer correctly with 3 parameters', () => {
const a = iif(() => false, of(1), of(2)); // $ExpectType Observable<number>
it('should error for incorrect parameters', () => {
const r0 = iif(() => 132, a$, b$); // $ExpectError
const r1 = iif(randomBoolean, {}, b$); // $ExpectError
const r2 = iif(randomBoolean, a$, {}); // $ExpectError
});

it('should infer correctly with 3 parameters of different types', () => {
const a = iif(() => false, of(1), of('a')); // $ExpectType Observable<string | number>
it('should infer correctly', () => {
const r0 = iif(() => false, a$, b$); // $ExpectType Observable<B>
const r1 = iif(() => true, a$, b$); // $ExpectType Observable<A>
const r2 = iif(randomBoolean, a$, b$); // $ExpectType Observable<A | B>
const r3 = iif(() => false, a$, EMPTY); // $ExpectType Observable<never>
const r4 = iif(() => true, EMPTY, b$); // $ExpectType Observable<never>
const r5 = iif(randomBoolean, EMPTY, EMPTY); // $ExpectType Observable<never>
});
12 changes: 6 additions & 6 deletions spec/observables/if-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { expectObservable } from '../helpers/marble-testing';

describe('iif', () => {
it('should subscribe to thenSource when the conditional returns true', () => {
const e1 = iif(() => true, of('a'));
const e1 = iif(() => true, of('a'), of());
const expected = '(a|)';

expectObservable(e1).toBe(expected);
Expand All @@ -18,16 +18,16 @@ describe('iif', () => {
});

it('should complete without an elseSource when the conditional returns false', () => {
const e1 = iif(() => false, of('a'));
const e1 = iif(() => false, of('a'), of());
const expected = '|';

expectObservable(e1).toBe(expected);
});

it('should raise error when conditional throws', () => {
const e1 = iif(<any>(() => {
const e1 = iif(((): boolean => {
throw 'error';
}), of('a'));
}), of('a'), of());

const expected = '#';

Expand All @@ -36,7 +36,7 @@ describe('iif', () => {

it('should accept resolved promise as thenSource', (done: MochaDone) => {
const expected = 42;
const e1 = iif(() => true, new Promise((resolve: any) => { resolve(expected); }));
const e1 = iif(() => true, new Promise((resolve: any) => { resolve(expected); }), of());

e1.subscribe(x => {
expect(x).to.equal(expected);
Expand Down Expand Up @@ -80,7 +80,7 @@ describe('iif', () => {

it('should accept rejected promise as thenSource', (done: MochaDone) => {
const expected = 42;
const e1 = iif(() => true, new Promise((resolve: any, reject: any) => { reject(expected); }));
const e1 = iif(() => true, new Promise((resolve: any, reject: any) => { reject(expected); }), of());

e1.subscribe(x => {
done(new Error('should not be called'));
Expand Down
51 changes: 23 additions & 28 deletions src/internal/observable/iif.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
/** @prettier */
import { Observable } from '../Observable';
import { defer } from './defer';
import { EMPTY } from './empty';
import { ObservableInput } from '../types';

export function iif<T>(condition: () => true, trueResult: ObservableInput<T>, falseResult: ObservableInput<any>): Observable<T>;

export function iif<F>(condition: () => false, trueResult: ObservableInput<any>, falseResult: ObservableInput<F>): Observable<F>;

export function iif<T, F>(condition: () => boolean, trueResult: ObservableInput<T>, falseResult: ObservableInput<F>): Observable<T | F>;

/**
* Decides at subscription time which Observable will actually be subscribed.
*
* <span class="informal">`If` statement for Observables.</span>
* Checks a boolean at subscription time, and chooses between one of two observable sources
*
* `iif` accepts a condition function and two Observables. When
* an Observable returned by the operator is subscribed, condition function will be called.
* Based on what boolean it returns at that moment, consumer will subscribe either to
* the first Observable (if condition was true) or to the second (if condition was false). Condition
* function may also not return anything - in that case condition will be evaluated as false and
* second Observable will be subscribed.
* `iif` excepts a function that returns a boolean (the `condition` function), and two sources,
* the `trueResult` and the `falseResult`, and returns an Observable.
*
* Note that Observables for both cases (true and false) are optional. If condition points to an Observable that
* was left undefined, resulting stream will simply complete immediately. That allows you to, rather
* than controlling which Observable will be subscribed, decide at runtime if consumer should have access
* to given Observable or not.
*
* If you have more complex logic that requires decision between more than two Observables, {@link defer}
* will probably be a better choice. Actually `iif` can be easily implemented with {@link defer}
* and exists only for convenience and readability reasons.
* At the moment of subscription, the `condition` function is called. If the result is `true`, the
* subscription will be to the source passed as the `trueResult`, otherwise, the subscription will be
* to the source passed as the `falseResult`.
*
* If you need to check more than two options to choose between more than one observable, have a look at the {@link defer} creation method.
*
* ## Examples
*
* ### Change at runtime which Observable will be subscribed
*
* ```ts
* import { iif, of } from 'rxjs';
*
Expand All @@ -52,6 +50,7 @@ import { ObservableInput } from '../types';
* ```
*
* ### Control an access to an Observable
*
* ```ts
* let accessGranted;
* const observableIfYouHaveAccess = iif(
Expand Down Expand Up @@ -83,15 +82,11 @@ import { ObservableInput } from '../types';
*
* @see {@link defer}
*
* @param {function(): boolean} condition Condition which Observable should be chosen.
* @param {Observable} [trueObservable] An Observable that will be subscribed if condition is true.
* @param {Observable} [falseObservable] An Observable that will be subscribed if condition is false.
* @return {Observable} Either first or second Observable, depending on condition.
* @param condition Condition which Observable should be chosen.
* @param trueResult An Observable that will be subscribed if condition is true.
* @param falseResult An Observable that will be subscribed if condition is false.
* @return An observable that proxies to `trueResult` or `falseResult`, depending on the result of the `condition` function.
*/
export function iif<T = never, F = never>(
condition: () => boolean,
trueResult: ObservableInput<T> = EMPTY,
falseResult: ObservableInput<F> = EMPTY
): Observable<T|F> {
return defer(() => condition() ? trueResult : falseResult);
export function iif<T, F>(condition: () => boolean, trueResult: ObservableInput<T>, falseResult: ObservableInput<F>): Observable<T | F> {
return defer(() => (condition() ? trueResult : falseResult));
}