From 5f2e8491b1a12a80f29f1f0d54d7fe30c10eaba9 Mon Sep 17 00:00:00 2001 From: Robert Raiford Date: Tue, 6 Dec 2016 20:06:41 -0500 Subject: [PATCH] fix(typings): type guard support for `last`, `first`, `find` and `filter`. * 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 --- spec/operators/filter-spec.ts | 51 ++++++++++++++++++++++++ spec/operators/find-spec.ts | 53 ++++++++++++++++++++++++- spec/operators/first-spec.ts | 71 +++++++++++++++++++++++++++++++++ spec/operators/last-spec.ts | 75 ++++++++++++++++++++++++++++++++++- src/operator/filter.ts | 6 ++- src/operator/find.ts | 6 ++- src/operator/first.ts | 26 +++++++----- src/operator/last.ts | 25 ++++++++---- 8 files changed, 290 insertions(+), 23 deletions(-) diff --git a/spec/operators/filter-spec.ts b/spec/operators/filter-spec.ts index a4b42121c7..bb3ec60f1a 100644 --- a/spec/operators/filter-spec.ts +++ b/spec/operators/filter-spec.ts @@ -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 && (x).bar !== undefined; + const isBaz = (x: any): x is Baz => x && (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 = 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 + }); }); diff --git a/spec/operators/find-spec.ts b/spec/operators/find-spec.ts index e01e1eefa2..e15956f391 100644 --- a/spec/operators/find-spec.ts +++ b/spec/operators/find-spec.ts @@ -155,4 +155,55 @@ describe('Observable.prototype.find', () => { expectObservable((source).find(predicate)).toBe(expected); expectSubscriptions(source.subscriptions).toBe(subs); }); -}); \ No newline at end of file + + 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 && (x).bar !== undefined; + const isBaz = (x: any): x is Baz => x && (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 = 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 + }); +}); diff --git a/spec/operators/first-spec.ts b/spec/operators/first-spec.ts index 6abad29eee..eebc2da02a 100644 --- a/spec/operators/first-spec.ts +++ b/spec/operators/first-spec.ts @@ -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 && (x).bar !== undefined; + const isBaz = (x: any): x is Baz => x && (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 = 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 + }); }); diff --git a/spec/operators/last-spec.ts b/spec/operators/last-spec.ts index be5476d89f..759c6d5ecf 100644 --- a/spec/operators/last-spec.ts +++ b/spec/operators/last-spec.ts @@ -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', () => { @@ -142,4 +144,75 @@ describe('Observable.prototype.last', () => { expectObservable(e1.last(predicate, resultSelector)).toBe(expected); expectSubscriptions(e1.subscriptions).toBe(e1subs); }); -}); \ No newline at end of file + + 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 && (x).bar !== undefined; + const isBaz = (x: any): x is Baz => x && (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 = 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 + }); +}); diff --git a/src/operator/filter.ts b/src/operator/filter.ts index be3f371eaa..4b47bc1d98 100644 --- a/src/operator/filter.ts +++ b/src/operator/filter.ts @@ -5,9 +5,11 @@ import { TeardownLogic } from '../Subscription'; /* tslint:disable:max-line-length */ export function filter(this: Observable, - predicate: ((value: T, index: number) => boolean) | - ((value: T, index: number) => value is S), + predicate: (value: T, index: number) => value is S, thisArg?: any): Observable; +export function filter(this: Observable, + predicate: (value: T, index: number) => boolean, + thisArg?: any): Observable; /* tslint:disable:max-line-length */ /** diff --git a/src/operator/find.ts b/src/operator/find.ts index 911f144d0b..fedbe1c7ac 100644 --- a/src/operator/find.ts +++ b/src/operator/find.ts @@ -4,9 +4,11 @@ import { Subscriber } from '../Subscriber'; /* tslint:disable:max-line-length */ export function find(this: Observable, - predicate: ((value: T, index: number, source: Observable) => boolean) | - ((value: T, index: number, source: Observable) => value is S), + predicate: (value: T, index: number) => value is S, thisArg?: any): Observable; +export function find(this: Observable, + predicate: (value: T, index: number) => boolean, + thisArg?: any): Observable; /* tslint:disable:max-line-length */ /** diff --git a/src/operator/first.ts b/src/operator/first.ts index 4a54a32e72..ab95b154b1 100644 --- a/src/operator/first.ts +++ b/src/operator/first.ts @@ -5,16 +5,24 @@ import { EmptyError } from '../util/EmptyError'; /* tslint:disable:max-line-length */ export function first(this: Observable, - predicate?: ((value: T, index: number, source: Observable) => boolean) | - ((value: T, index: number, source: Observable) => value is S)): Observable; -export function first(this: Observable, predicate: (value: T, index: number, source: Observable) => boolean, resultSelector: void, defaultValue?: T): Observable; + predicate: (value: T, index: number, source: Observable) => value is S): Observable; export function first(this: Observable, - predicate: ((value: T, index: number, source: Observable) => boolean) | - ((value: T, index: number, source: Observable) => value is S), - resultSelector?: ((value: S, index: number) => R) | void, - defaultValue?: S): Observable; -export function first(this: Observable, predicate?: (value: T, index: number, source: Observable) => boolean, resultSelector?: (value: T, index: number) => R, defaultValue?: R): Observable; -/* tslint:disable:max-line-length */ + predicate: (value: T | S, index: number, source: Observable) => value is S, + resultSelector: (value: S, index: number) => R, defaultValue?: R): Observable; +export function first(this: Observable, + predicate: (value: T, index: number, source: Observable) => value is S, + resultSelector: void, + defaultValue?: S): Observable; +export function first(this: Observable, + predicate?: (value: T, index: number, source: Observable) => boolean): Observable; +export function first(this: Observable, + predicate: (value: T, index: number, source: Observable) => boolean, + resultSelector?: (value: T, index: number) => R, + defaultValue?: R): Observable; +export function first(this: Observable, + predicate: (value: T, index: number, source: Observable) => boolean, + resultSelector: void, + defaultValue?: T): Observable; /** * Emits only the first value (or the first value that meets some condition) diff --git a/src/operator/last.ts b/src/operator/last.ts index 9b9d898ce6..a5db39e304 100644 --- a/src/operator/last.ts +++ b/src/operator/last.ts @@ -5,15 +5,24 @@ import { EmptyError } from '../util/EmptyError'; /* tslint:disable:max-line-length */ export function last(this: Observable, - predicate?: ((value: T, index: number, source: Observable) => boolean) | - ((value: T, index: number, source: Observable) => value is S)): Observable; -export function last(this: Observable, predicate: (value: T, index: number, source: Observable) => boolean, resultSelector: void, defaultValue?: T): Observable; + predicate: (value: T, index: number, source: Observable) => value is S): Observable; export function last(this: Observable, - predicate: ((value: T, index: number, source: Observable) => boolean) | - ((value: T, index: number, source: Observable) => value is S), - resultSelector?: ((value: S, index: number) => R) | void, - defaultValue?: S): Observable; -export function last(this: Observable, predicate?: (value: T, index: number, source: Observable) => boolean, resultSelector?: (value: T, index: number) => R, defaultValue?: R): Observable; + predicate: (value: T | S, index: number, source: Observable) => value is S, + resultSelector: (value: S, index: number) => R, defaultValue?: R): Observable; +export function last(this: Observable, + predicate: (value: T, index: number, source: Observable) => value is S, + resultSelector: void, + defaultValue?: S): Observable; +export function last(this: Observable, + predicate?: (value: T, index: number, source: Observable) => boolean): Observable; +export function last(this: Observable, + predicate: (value: T, index: number, source: Observable) => boolean, + resultSelector?: (value: T, index: number) => R, + defaultValue?: R): Observable; +export function last(this: Observable, + predicate: (value: T, index: number, source: Observable) => boolean, + resultSelector: void, + defaultValue?: T): Observable; /* tslint:disable:max-line-length */ /**