Skip to content

Commit

Permalink
fix(typings): type guard support for last, first, find and `fil…
Browse files Browse the repository at this point in the history
…ter`.

* fix(typings): type guard support for `last`

* fix(typings): type guard support for `first`

* fix(typings): type guard support for `find`

* fix(typings): type guard support for `filter`

* style(missing-semicolons): add missing semicolons in filter, find, first, last type guard specs
  • Loading branch information
rob3c authored and benlesh committed Dec 7, 2016
1 parent 86a909c commit 5f2e849
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 23 deletions.
51 changes: 51 additions & 0 deletions spec/operators/filter-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,4 +272,55 @@ describe('Observable.prototype.filter', () => {
expectObservable(r, unsub).toBe(expected);
expectSubscriptions(source.subscriptions).toBe(subs);
});

it('should support type guards without breaking previous behavior', () => {
// tslint:disable no-unused-variable

// type guards with interfaces and classes
{
interface Bar { bar?: string; }
interface Baz { baz?: number; }
class Foo implements Bar, Baz { constructor(public bar: string = 'name', public baz: number = 42) {} }

const isBar = (x: any): x is Bar => x && (<Bar>x).bar !== undefined;
const isBaz = (x: any): x is Baz => x && (<Baz>x).baz !== undefined;

const foo: Foo = new Foo();
Observable.of(foo).filter(foo => foo.baz === 42)
.subscribe(x => x.baz); // x is still Foo
Observable.of(foo).filter(isBar)
.subscribe(x => x.bar); // x is Bar!

const foobar: Bar = new Foo(); // type is interface, not the class
Observable.of(foobar).filter(foobar => foobar.bar === 'name')
.subscribe(x => x.bar); // <-- x is still Bar
Observable.of(foobar).filter(isBar)
.subscribe(x => x.bar); // <--- x is Bar!

const barish = { bar: 'quack', baz: 42 }; // type can quack like a Bar
Observable.of(barish).filter(x => x.bar === 'quack')
.subscribe(x => x.bar); // x is still { bar: string; baz: number; }
Observable.of(barish).filter(isBar)
.subscribe(bar => bar.bar); // x is Bar!
}

// type guards with primitive types
{
const xs: 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';

xs.filter(isString)
.subscribe(s => s.length); // s is string

// In contrast, this type of regular boolean predicate still maintains the original type
xs.filter(x => typeof x === 'number')
.subscribe(x => x); // x is still string | number
xs.filter((x, i) => typeof x === 'number' && x > i)
.subscribe(x => x); // x is still string | number
}

// tslint:disable enable
});
});
53 changes: 52 additions & 1 deletion spec/operators/find-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,55 @@ describe('Observable.prototype.find', () => {
expectObservable((<any>source).find(predicate)).toBe(expected);
expectSubscriptions(source.subscriptions).toBe(subs);
});
});

it('should support type guards without breaking previous behavior', () => {
// tslint:disable no-unused-variable

// type guards with interfaces and classes
{
interface Bar { bar?: string; }
interface Baz { baz?: number; }
class Foo implements Bar, Baz { constructor(public bar: string = 'name', public baz: number = 42) {} }

const isBar = (x: any): x is Bar => x && (<Bar>x).bar !== undefined;
const isBaz = (x: any): x is Baz => x && (<Baz>x).baz !== undefined;

const foo: Foo = new Foo();
Observable.of(foo).find(foo => foo.baz === 42)
.subscribe(x => x.baz); // x is still Foo
Observable.of(foo).find(isBar)
.subscribe(x => x.bar); // x is Bar!

const foobar: Bar = new Foo(); // type is interface, not the class
Observable.of(foobar).find(foobar => foobar.bar === 'name')
.subscribe(x => x.bar); // <-- x is still Bar
Observable.of(foobar).find(isBar)
.subscribe(x => x.bar); // <--- x is Bar!

const barish = { bar: 'quack', baz: 42 }; // type can quack like a Bar
Observable.of(barish).find(x => x.bar === 'quack')
.subscribe(x => x.bar); // x is still { bar: string; baz: number; }
Observable.of(barish).find(isBar)
.subscribe(bar => bar.bar); // x is Bar!
}

// type guards with primitive types
{
const xs: 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';

xs.find(isString)
.subscribe(s => s.length); // s is string

// In contrast, this type of regular boolean predicate still maintains the original type
xs.find(x => typeof x === 'number')
.subscribe(x => x); // x is still string | number
xs.find((x, i) => typeof x === 'number' && x > i)
.subscribe(x => x); // x is still string | number
}

// tslint:disable enable
});
});
71 changes: 71 additions & 0 deletions spec/operators/first-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,75 @@ describe('Observable.prototype.first', () => {
expectObservable(e1.first(predicate, resultSelector)).toBe(expected);
expectSubscriptions(e1.subscriptions).toBe(sub);
});

it('should support type guards without breaking previous behavior', () => {
// tslint:disable no-unused-variable

// type guards with interfaces and classes
{
interface Bar { bar?: string; }
interface Baz { baz?: number; }
class Foo implements Bar, Baz { constructor(public bar: string = 'name', public baz: number = 42) {} }

const isBar = (x: any): x is Bar => x && (<Bar>x).bar !== undefined;
const isBaz = (x: any): x is Baz => x && (<Baz>x).baz !== undefined;

const foo: Foo = new Foo();
Observable.of(foo).first()
.subscribe(x => x.baz); // x is Foo
Observable.of(foo).first(foo => foo.bar === 'name')
.subscribe(x => x.baz); // x is still Foo
Observable.of(foo).first(isBar)
.subscribe(x => x.bar); // x is Bar!

const foobar: Bar = new Foo(); // type is the interface, not the class
Observable.of(foobar).first()
.subscribe(x => x.bar); // x is Bar
Observable.of(foobar).first(foobar => foobar.bar === 'name')
.subscribe(x => x.bar); // x is still Bar
Observable.of(foobar).first(isBaz)
.subscribe(x => x.baz); // x is Baz!

const barish = { bar: 'quack', baz: 42 }; // type can quack like a Bar
Observable.of(barish).first()
.subscribe(x => x.baz); // x is still { bar: string; baz: number; }
Observable.of(barish).first(x => x.bar === 'quack')
.subscribe(x => x.bar); // x is still { bar: string; baz: number; }
Observable.of(barish).first(isBar)
.subscribe(x => x.bar); // x is Bar!
}

// type guards with primitive types
{
const xs: 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';

// missing predicate preserves the type
xs.first().subscribe(x => x); // x is still string | number

// After the type guard `first` predicates, the type is narrowed to string
xs.first(isString)
.subscribe(s => s.length); // s is string
xs.first(isString, s => s.substr(0)) // s is string in predicate
.subscribe(s => s.length); // s is string

// boolean predicates preserve the type
xs.first(x => typeof x === 'string')
.subscribe(x => x); // x is still string | number
xs.first(x => !!x, x => x)
.subscribe(x => x); // x is still string | number
xs.first(x => typeof x === 'string', x => x, '') // default is string; x remains string | number
.subscribe(x => x); // x is still string | number

// `first` still uses the `resultSelector` return type, if it exists.
xs.first(x => typeof x === 'string', x => ({ str: `${x}` })) // x remains string | number
.subscribe(o => o.str); // o is { str: string }
xs.first(x => typeof x === 'string', x => ({ str: `${x}` }), { str: '' })
.subscribe(o => o.str); // o is { str: string }
}

// tslint:disable enable
});
});
75 changes: 74 additions & 1 deletion spec/operators/last-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {expect} from 'chai';
import * as Rx from '../../dist/cjs/Rx';
declare const {hot, cold, asDiagram, expectObservable, expectSubscriptions};

const Observable = Rx.Observable;

/** @test {last} */
describe('Observable.prototype.last', () => {
asDiagram('last')('should take the last value of an observable', () => {
Expand Down Expand Up @@ -142,4 +144,75 @@ describe('Observable.prototype.last', () => {
expectObservable(e1.last(predicate, resultSelector)).toBe(expected);
expectSubscriptions(e1.subscriptions).toBe(e1subs);
});
});

it('should support type guards without breaking previous behavior', () => {
// tslint:disable no-unused-variable

// type guards with interfaces and classes
{
interface Bar { bar?: string; }
interface Baz { baz?: number; }
class Foo implements Bar, Baz { constructor(public bar: string = 'name', public baz: number = 42) {} }

const isBar = (x: any): x is Bar => x && (<Bar>x).bar !== undefined;
const isBaz = (x: any): x is Baz => x && (<Baz>x).baz !== undefined;

const foo: Foo = new Foo();
Observable.of(foo).last()
.subscribe(x => x.baz); // x is Foo
Observable.of(foo).last(foo => foo.bar === 'name')
.subscribe(x => x.baz); // x is still Foo
Observable.of(foo).last(isBar)
.subscribe(x => x.bar); // x is Bar!

const foobar: Bar = new Foo(); // type is the interface, not the class
Observable.of(foobar).last()
.subscribe(x => x.bar); // x is Bar
Observable.of(foobar).last(foobar => foobar.bar === 'name')
.subscribe(x => x.bar); // x is still Bar
Observable.of(foobar).last(isBaz)
.subscribe(x => x.baz); // x is Baz!

const barish = { bar: 'quack', baz: 42 }; // type can quack like a Bar
Observable.of(barish).last()
.subscribe(x => x.baz); // x is still { bar: string; baz: number; }
Observable.of(barish).last(x => x.bar === 'quack')
.subscribe(x => x.bar); // x is still { bar: string; baz: number; }
Observable.of(barish).last(isBar)
.subscribe(x => x.bar); // x is Bar!
}

// type guards with primitive types
{
const xs: 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';

// missing predicate preserves the type
xs.last().subscribe(x => x); // x is still string | number

// After the type guard `last` predicates, the type is narrowed to string
xs.last(isString)
.subscribe(s => s.length); // s is string
xs.last(isString, s => s.substr(0)) // s is string in predicate
.subscribe(s => s.length); // s is string

// boolean predicates preserve the type
xs.last(x => typeof x === 'string')
.subscribe(x => x); // x is still string | number
xs.last(x => !!x, x => x)
.subscribe(x => x); // x is still string | number
xs.last(x => typeof x === 'string', x => x, '') // default is string; x remains string | number
.subscribe(x => x); // x is still string | number

// `last` still uses the `resultSelector` return type, if it exists.
xs.last(x => typeof x === 'string', x => ({ str: `${x}` })) // x remains string | number
.subscribe(o => o.str); // o is { str: string }
xs.last(x => typeof x === 'string', x => ({ str: `${x}` }), { str: '' })
.subscribe(o => o.str); // o is { str: string }
}

// tslint:disable enable
});
});
6 changes: 4 additions & 2 deletions src/operator/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { TeardownLogic } from '../Subscription';

/* tslint:disable:max-line-length */
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>;
export function filter<T>(this: Observable<T>,
predicate: (value: T, index: number) => boolean,
thisArg?: any): Observable<T>;
/* tslint:disable:max-line-length */

/**
Expand Down
6 changes: 4 additions & 2 deletions src/operator/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { Subscriber } from '../Subscriber';

/* tslint:disable:max-line-length */
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) => value is S,
thisArg?: any): Observable<S>;
export function find<T>(this: Observable<T>,
predicate: (value: T, index: number) => boolean,
thisArg?: any): Observable<T>;
/* tslint:disable:max-line-length */

/**
Expand Down
26 changes: 17 additions & 9 deletions src/operator/first.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,24 @@ import { EmptyError } from '../util/EmptyError';

/* tslint:disable:max-line-length */
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>;
predicate: (value: T, index: number, source: Observable<T>) => value is S): Observable<S>;
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 | S, index: number, source: Observable<T>) => value is S,
resultSelector: (value: S, index: number) => R, defaultValue?: R): Observable<R>;
export function first<T, S extends T>(this: Observable<T>,
predicate: (value: T, index: number, source: Observable<T>) => value is S,
resultSelector: void,
defaultValue?: S): Observable<S>;
export function first<T>(this: Observable<T>,
predicate?: (value: T, index: number, source: Observable<T>) => boolean): Observable<T>;
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>;
export function first<T>(this: Observable<T>,
predicate: (value: T, index: number, source: Observable<T>) => boolean,
resultSelector: void,
defaultValue?: T): Observable<T>;

/**
* Emits only the first value (or the first value that meets some condition)
Expand Down
25 changes: 17 additions & 8 deletions src/operator/last.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,24 @@ import { EmptyError } from '../util/EmptyError';

/* tslint:disable:max-line-length */
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>;
predicate: (value: T, index: number, source: Observable<T>) => value is S): Observable<S>;
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>;
predicate: (value: T | S, index: number, source: Observable<T>) => value is S,
resultSelector: (value: S, index: number) => R, defaultValue?: R): Observable<R>;
export function last<T, S extends T>(this: Observable<T>,
predicate: (value: T, index: number, source: Observable<T>) => value is S,
resultSelector: void,
defaultValue?: S): Observable<S>;
export function last<T>(this: Observable<T>,
predicate?: (value: T, index: number, source: Observable<T>) => boolean): Observable<T>;
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>;
export function last<T>(this: Observable<T>,
predicate: (value: T, index: number, source: Observable<T>) => boolean,
resultSelector: void,
defaultValue?: T): Observable<T>;
/* tslint:disable:max-line-length */

/**
Expand Down

0 comments on commit 5f2e849

Please sign in to comment.