Skip to content

fix(typings): Use overloading instead of union type for typings filter, find, first, last. #2164

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

Closed
wants to merge 2 commits into from
Closed
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
34 changes: 33 additions & 1 deletion spec/operators/filter-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,4 +272,36 @@ describe('Observable.prototype.filter', () => {
expectObservable(r, unsub).toBe(expected);
expectSubscriptions(source.subscriptions).toBe(subs);
});
});

it('should not be compile error', () => {
{
// x is `Observable<string | number>`
const x: Rx.Observable<string | number> = Observable.from([1, 'aaa', 3, 'bb']);
// This type guard will narrow a `string | number` to a string in the examples below
const isString = (x: string | number): x is string => typeof x === 'string';

// Here, `s` is a string in the second filter predicate after the type guard (yay - intellisense!)
const guardedFilter = x.filter<string | number, string>(isString).filter(s => s.length === 2); // Observable<string>
// In contrast, this type of regular boolean predicate still maintains the original type
const boolFilter = x.filter(s => typeof s === 'number'); // Observable<string | number>

// To avoid the lint error about unused variables
expect(guardedFilter).to.not.equal(true);
expect(boolFilter).to.not.equal(true);
}

{
interface Bar {
bar?: string;
}
class Foo implements Bar {
constructor(public bar: string = 'name') {}
}

let foo: Bar = new Foo(); // <--- type is interface, not the class
Observable.of(foo)
.filter(foo => foo.bar === 'name')
.subscribe(foo => console.log(foo.bar)); // <-- "Property 'bar' does not exist on type '{}'"
}
});
});
18 changes: 18 additions & 0 deletions spec/operators/find-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,22 @@ describe('Observable.prototype.find', () => {
expectObservable((<any>source).find(predicate)).toBe(expected);
expectSubscriptions(source.subscriptions).toBe(subs);
});

it('should not be compile error', () => {
{
// x is `Observable<string | number>`
const x: Rx.Observable<string | number> = Observable.from([1, 'aaa', 3, 'bb']);
// This type guard will narrow a `string | number` to a string in the examples below
const isString = (x: string | number): x is string => typeof x === 'string';

// After the type guard `find` predicate, the type is narrowed to string
const guardedFind = x.find<string | number, string>(isString).filter(s => s.length > 1).map(s => s.substr(1)); // Observable<string>
// In contrast, a boolean predicate maintains the original type
const boolFind = x.find(x => typeof x === 'string'); // Observable<string | number>

// To avoid the lint error about unused variables
expect(guardedFind).to.not.equal(true);
expect(boolFind).to.not.equal(true);
}
});
});
23 changes: 23 additions & 0 deletions spec/operators/first-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,27 @@ describe('Observable.prototype.first', () => {
expectObservable(e1.first(predicate, resultSelector)).toBe(expected);
expectSubscriptions(e1.subscriptions).toBe(sub);
});

it('should not be compile error', () => {
{
// x is `Observable<string | number>`
const x: Rx.Observable<string | number> = Observable.from([1, 'aaa', 3, 'bb']);
// This type guard will narrow a `string | number` to a string in the examples below
const isString = (x: string | number): x is string => typeof x === 'string';

// After the type guard `first` predicates, the type is narrowed to string
const guardedFirst1 = x.first<string | number, string>(isString).filter(s => s.length > 1).map(s => s.substr(1)); // Observable<string>
const guardedFirst2 = x.first<string | number, string>(isString, s => s.substr(0)).filter(s => s.length > 1); // Observable<string>
// Without a resultSelector, `first` maintains the original type (TS can't do this yet)
const boolFirst1 = x.first(x => typeof x === 'string', null, ''); // Observable<string | number>
// `first` still uses the `resultSelector` return type, if it exists.
const boolFirst2 = x.first(x => typeof x === 'string', s => ({str: `${s}`}), {str: ''}); // Observable<{str: string}>

// To avoid the lint error about unused variables
expect(guardedFirst1).to.not.equal(true);
expect(guardedFirst2).to.not.equal(true);
expect(boolFirst1).to.not.equal(true);
expect(boolFirst2).to.not.equal(true);
}
});
});
23 changes: 23 additions & 0 deletions spec/operators/last-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,27 @@ describe('Observable.prototype.last', () => {
expectObservable(e1.last(predicate, resultSelector)).toBe(expected);
expectSubscriptions(e1.subscriptions).toBe(e1subs);
});

it('should not be compile error', () => {
{
// x is `Observable<string | number>`
const x: Rx.Observable<string | number> = Rx.Observable.from([1, 'aaa', 3, 'bb']);
// This type guard will narrow a `string | number` to a string in the examples below
const isString = (x: string | number): x is string => typeof x === 'string';

// After the type guard `last` predicates, the type is narrowed to string
const guardedLast1 = x.last<string | number, string>(isString).filter(s => s.length > 1).map(s => s.substr(1)); // Observable<string>
const guardedLast2 = x.last<string | number, string>(isString, s => s.substr(0)).filter(s => s.length > 1); // Observable<string>
// Without a resultSelector, `last` maintains the original type (TS can't do this yet)
const boolLast1 = x.last(x => typeof x === 'string', null, ''); // Observable<string | number>
// `last` still uses the `resultSelector` return type, if it exists.
const boolLast2 = x.last(x => typeof x === 'string', s => ({str: `${s}`}), {str: ''}); // Observable<{str: string}>

// To avoid the lint error about unused variables
expect(guardedLast1).to.not.equal(true);
expect(guardedLast2).to.not.equal(true);
expect(boolLast1).to.not.equal(true);
expect(boolLast2).to.not.equal(true);
}
});
});
11 changes: 7 additions & 4 deletions src/operator/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import { Subscriber } from '../Subscriber';
import { Observable } from '../Observable';
import { TeardownLogic } from '../Subscription';

/* tslint:disable:max-line-length */
// XXX: At [email protected], we need prepare the version which takes `predicate`
// returning `boolean` before to define the one which takes the type guard `predicate`
// so that the type inference works correctly for the case of that `predicate` returning `boolean` simply.
export function filter<T>(this: Observable<T>,
predicate: (value: T, index: number) => boolean,
thisArg?: any): Observable<T>;
export function filter<T, S extends T>(this: Observable<T>,
predicate: ((value: T, index: number) => boolean) |
((value: T, index: number) => value is S),
predicate: (value: T, index: number) => value is S,
thisArg?: any): Observable<S>;
/* tslint:disable:max-line-length */

/**
* Filter items emitted by the source Observable by only emitting those that
Expand Down
11 changes: 7 additions & 4 deletions src/operator/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { Observable } from '../Observable';
import { Operator } from '../Operator';
import { Subscriber } from '../Subscriber';

/* tslint:disable:max-line-length */
// XXX: At [email protected], we need prepare the version which takes `predicate`
// returning `boolean` before to define the one which takes the type guard `predicate`
// so that the type inference works correctly for the case of that `predicate` returning `boolean` simply.
export function find<T>(this: Observable<T>,
predicate: (value: T, index: number, source: Observable<T>) => boolean,
thisArg?: any): Observable<T>;
export function find<T, S extends T>(this: Observable<T>,
predicate: ((value: T, index: number, source: Observable<T>) => boolean) |
((value: T, index: number, source: Observable<T>) => value is S),
predicate: (value: T, index: number, source: Observable<T>) => value is S,
thisArg?: any): Observable<S>;
/* tslint:disable:max-line-length */

/**
* Emits only the first value emitted by the source Observable that meets some
Expand Down
31 changes: 19 additions & 12 deletions src/operator/first.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,26 @@ import { Operator } from '../Operator';
import { Subscriber } from '../Subscriber';
import { EmptyError } from '../util/EmptyError';

/* tslint:disable:max-line-length */
// XXX: At [email protected], we need prepare the version which takes `predicate`
// returning `boolean` before to define the one which takes the type guard `predicate`
// so that the type inference works correctly for the case of that `predicate` returning `boolean` simply.
export function first<T>(this: Observable<T>,
predicate?: (value: T, index: number,
source: Observable<T>) => boolean): Observable<T>;
export function first<T, S extends T>(this: Observable<T>,
predicate?: ((value: T, index: number, source: Observable<T>) => boolean) |
((value: T, index: number, source: Observable<T>) => value is S)): Observable<S>;
export function first<T>(this: Observable<T>, predicate: (value: T, index: number, source: Observable<T>) => boolean, resultSelector: void, defaultValue?: T): Observable<T>;
export function first<T, S extends T, R>(this: Observable<T>,
predicate: ((value: T, index: number, source: Observable<T>) => boolean) |
((value: T, index: number, source: Observable<T>) => value is S),
resultSelector?: ((value: S, index: number) => R) | void,
defaultValue?: S): Observable<S>;
export function first<T, R>(this: Observable<T>, predicate?: (value: T, index: number, source: Observable<T>) => boolean, resultSelector?: (value: T, index: number) => R, defaultValue?: R): Observable<R>;
/* tslint:disable:max-line-length */

predicate?: (value: T, index: number,
source: Observable<T>) => value is S): Observable<S>;
export function first<T>(this: Observable<T>,
predicate: (value: T, index: number, source: Observable<T>) => boolean,
resultSelector: (value: T, index: number) => T,
defaultValue?: T): Observable<T>;
export function first<T, S extends T>(this: Observable<T>,
predicate: (value: T, index: number, source: Observable<T>) => value is S,
resultSelector: (value: S, index: number) => S,
defaultValue?: S): Observable<S>;
export function first<T, R>(this: Observable<T>, predicate?: (value: T, index: number, source: Observable<T>) => boolean,
resultSelector?: (value: T, index: number) => R,
defaultValue?: R): Observable<R>;
/**
* Emits only the first value (or the first value that meets some condition)
* emitted by the source Observable.
Expand Down
32 changes: 21 additions & 11 deletions src/operator/last.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,28 @@ import { Operator } from '../Operator';
import { Subscriber } from '../Subscriber';
import { EmptyError } from '../util/EmptyError';

/* tslint:disable:max-line-length */
// XXX: At [email protected], we need prepare the version which takes `predicate`
// returning `boolean` before to define the one which takes the type guard `predicate`
// so that the type inference works correctly for the case of that `predicate` returning `boolean` simply.
export function last<T>(this: Observable<T>,
predicate?: (value: T, index: number,
source: Observable<T>) => boolean): Observable<T>;
export function last<T, S extends T>(this: Observable<T>,
predicate?: ((value: T, index: number, source: Observable<T>) => boolean) |
((value: T, index: number, source: Observable<T>) => value is S)): Observable<S>;
export function last<T>(this: Observable<T>, predicate: (value: T, index: number, source: Observable<T>) => boolean, resultSelector: void, defaultValue?: T): Observable<T>;
export function last<T, S extends T, R>(this: Observable<T>,
predicate: ((value: T, index: number, source: Observable<T>) => boolean) |
((value: T, index: number, source: Observable<T>) => value is S),
resultSelector?: ((value: S, index: number) => R) | void,
defaultValue?: S): Observable<S>;
export function last<T, R>(this: Observable<T>, predicate?: (value: T, index: number, source: Observable<T>) => boolean, resultSelector?: (value: T, index: number) => R, defaultValue?: R): Observable<R>;
/* tslint:disable:max-line-length */
predicate?: (value: T, index: number,
source: Observable<T>) => value is S): Observable<S>;
export function last<T>(this: Observable<T>,
predicate: (value: T, index: number,
source: Observable<T>) => boolean,
resultSelector: (value: T, index: number) => T,
defaultValue?: T): Observable<T>;
export function last<T, S extends T>(this: Observable<T>,
predicate: (value: T, index: number, source: Observable<T>) => value is S,
resultSelector: (value: S, index: number) => S,
defaultValue?: S): Observable<S>;
export function last<T, R>(this: Observable<T>,
predicate?: (value: T, index: number, source: Observable<T>) => boolean,
resultSelector?: (value: T, index: number) => R,
defaultValue?: R): Observable<R>;

/**
* Returns an Observable that emits only the last item emitted by the source Observable.
Expand Down