From 126d2b69bb93000968069691d108b6848632f80d Mon Sep 17 00:00:00 2001 From: David Driscoll Date: Thu, 9 May 2019 20:03:22 -0400 Subject: [PATCH] fix(scan): fixed declarations to properly support different return types (#4598) this solves an issue where if you would reduce over `T` you can get `T` out reliably, but you could not get out `R` in anyway without manually typing the reduce method call --- compat/operator/reduce.ts | 8 +++---- compat/operator/scan.ts | 12 +++++------ spec-dtslint/operators/reduce-spec.ts | 14 +++++++++++- spec-dtslint/operators/scan-spec.ts | 31 +++++++++++++++++++++++++++ spec/operators/reduce-spec.ts | 4 ++-- spec/operators/scan-spec.ts | 6 +++--- src/internal/operators/reduce.ts | 12 +++++------ src/internal/operators/scan.ts | 4 ++-- 8 files changed, 67 insertions(+), 24 deletions(-) create mode 100644 spec-dtslint/operators/scan-spec.ts diff --git a/compat/operator/reduce.ts b/compat/operator/reduce.ts index fd27ab4ca1..956174fb86 100644 --- a/compat/operator/reduce.ts +++ b/compat/operator/reduce.ts @@ -2,9 +2,9 @@ import { Observable } from 'rxjs'; import { reduce as higherOrderReduce } from 'rxjs/operators'; /* tslint:disable:max-line-length */ -export function reduce(this: Observable, accumulator: (acc: T, value: T, index: number) => T, seed?: T): Observable; -export function reduce(this: Observable, accumulator: (acc: T[], value: T, index: number) => T[], seed: T[]): Observable; export function reduce(this: Observable, accumulator: (acc: R, value: T, index: number) => R, seed: R): Observable; +export function reduce(this: Observable, accumulator: (acc: T, value: T, index: number) => T, seed?: T): Observable; +export function reduce(this: Observable, accumulator: (acc: R, value: T, index: number) => R): Observable; /* tslint:enable:max-line-length */ /** @@ -51,7 +51,7 @@ export function reduce(this: Observable, accumulator: (acc: R, value: T * @method reduce * @owner Observable */ -export function reduce(this: Observable, accumulator: (acc: R, value: T, index?: number) => R, seed?: R): Observable { +export function reduce(this: Observable, accumulator: (acc: T | R, value: T, index?: number) => (T | R), seed?: R): Observable { // providing a seed of `undefined` *should* be valid and trigger // hasSeed! so don't use `seed !== undefined` checks! // For this reason, we have to check it here at the original call site @@ -61,5 +61,5 @@ export function reduce(this: Observable, accumulator: (acc: R, value: T return higherOrderReduce(accumulator, seed)(this); } - return higherOrderReduce(accumulator)(this); + return higherOrderReduce(accumulator)(this); } diff --git a/compat/operator/scan.ts b/compat/operator/scan.ts index a4e19b965b..97c41b94fb 100644 --- a/compat/operator/scan.ts +++ b/compat/operator/scan.ts @@ -3,9 +3,9 @@ import { Observable } from 'rxjs'; import { scan as higherOrderScan } from 'rxjs/operators'; /* tslint:disable:max-line-length */ +export function scan(this: Observable, accumulator: (acc: R, value: T, index: number) => R, seed: R): Observable; export function scan(this: Observable, accumulator: (acc: T, value: T, index: number) => T, seed?: T): Observable; -export function scan(this: Observable, accumulator: (acc: T[], value: T, index: number) => T[], seed?: T[]): Observable; -export function scan(this: Observable, accumulator: (acc: R, value: T, index: number) => R, seed?: R): Observable; +export function scan(this: Observable, accumulator: (acc: R, value: T, index: number) => R): Observable; /* tslint:enable:max-line-length */ /** @@ -46,10 +46,10 @@ export function scan(this: Observable, accumulator: (acc: R, value: T, * @owner Observable */ export function scan(this: Observable, - accumulator: (acc: T | Array | R, value: T, index: number) => T | Array | R, - seed?: T | R): Observable { + accumulator: (acc: T | R, value: T, index: number) => T | R, + seed?: T | R): Observable { if (arguments.length >= 2) { - return higherOrderScan(accumulator, seed)(this) as Observable; + return higherOrderScan(accumulator, seed)(this); } - return higherOrderScan(accumulator)(this) as Observable; + return higherOrderScan(accumulator)(this); } diff --git a/spec-dtslint/operators/reduce-spec.ts b/spec-dtslint/operators/reduce-spec.ts index 9511e91ec4..5f31ed75ba 100644 --- a/spec-dtslint/operators/reduce-spec.ts +++ b/spec-dtslint/operators/reduce-spec.ts @@ -15,5 +15,17 @@ it('should infer correctly for accumulator of type array', () => { it('should accept seed parameter of the same type', () => { const a = of(1, 2, 3).pipe(reduce((x, y, z) => x + 1, 5)); // $ExpectType Observable - const b = of(1, 2, 3).pipe(reduce((x, y, z) => x + 1, '5')); // $ExpectError + const b = of(1, 2, 3).pipe(reduce((x, y, z) => x + 1, [])); // $ExpectError +}); + +it('should accept seed parameter of the seed array type', () => { + const a = of(1, 2, 3).pipe(reduce((x, y, z) => { x.push(y); return x; }, [4])); // $ExpectType Observable + // Array must be typed... + const b = of(1, 2, 3).pipe(reduce((x, y, z) => { x.push(y); return x; }, [])); // $ExpectError +}); + +it('should accept seed parameter of a different type', () => { + const a = of(1, 2, 3).pipe(reduce((x, y, z) => x + '1', '5')); // $ExpectType Observable + const bv: { [key: string]: string } = {}; + const b = of(1, 2, 3).pipe(reduce((x, y, z) => ({ ...x, [y]: y.toString() }), bv)); // $ExpectType Observable<{ [key: string]: string; }> }); diff --git a/spec-dtslint/operators/scan-spec.ts b/spec-dtslint/operators/scan-spec.ts new file mode 100644 index 0000000000..f5e73bf114 --- /dev/null +++ b/spec-dtslint/operators/scan-spec.ts @@ -0,0 +1,31 @@ +import { of, Observable } from 'rxjs'; +import { scan } from 'rxjs/operators'; + +it('should enforce parameter', () => { + const a = of(1, 2, 3).pipe(scan()); // $ExpectError +}); + +it('should infer correctly ', () => { + const a = of(1, 2, 3).pipe(scan((x, y, z) => x + 1)); // $ExpectType Observable +}); + +it('should infer correctly for accumulator of type array', () => { + const a = of(1, 2, 3).pipe(scan((x: number[], y: number, i: number) => x, [])); // $ExpectType Observable +}); + +it('should accept seed parameter of the same type', () => { + const a = of(1, 2, 3).pipe(scan((x, y, z) => x + 1, 5)); // $ExpectType Observable + const b = of(1, 2, 3).pipe(scan((x, y, z) => x + 1, [])); // $ExpectError +}); + +it('should accept seed parameter of the seed array type', () => { + const a = of(1, 2, 3).pipe(scan((x, y, z) => { x.push(y); return x; }, [4])); // $ExpectType Observable + // Array must be typed... + const b = of(1, 2, 3).pipe(scan((x, y, z) => { x.push(y); return x; }, [])); // $ExpectError +}); + +it('should accept seed parameter of a different type', () => { + const a = of(1, 2, 3).pipe(scan((x, y, z) => x + '1', '5')); // $ExpectType Observable + const bv: { [key: string]: string } = {}; + const b = of(1, 2, 3).pipe(scan((x, y, z) => ({ ...x, [y]: y.toString() }), bv)); // $ExpectType Observable<{ [key: string]: string; }> +}); diff --git a/spec/operators/reduce-spec.ts b/spec/operators/reduce-spec.ts index ddff7cb57c..4cbea42efc 100644 --- a/spec/operators/reduce-spec.ts +++ b/spec/operators/reduce-spec.ts @@ -292,7 +292,7 @@ describe('reduce operator', () => { type('should accept array typed reducers', () => { let a: Observable<{ a: number; b: string }>; - a.pipe(reduce<{ a: number; b: string }>((acc, value) => acc.concat(value), [])); + a.pipe(reduce((acc, value) => acc.concat(value), [])); }); type('should accept T typed reducers', () => { @@ -322,7 +322,7 @@ describe('reduce operator', () => { type('should accept R typed reduces when R is an array of T', () => { let a: Observable; - const reduced = a.pipe(reduce((acc, value) => { + const reduced = a.pipe(reduce((acc, value) => { acc.push(value); return acc; }, [])); diff --git a/spec/operators/scan-spec.ts b/spec/operators/scan-spec.ts index 363f9f54ad..66f4e2d9eb 100644 --- a/spec/operators/scan-spec.ts +++ b/spec/operators/scan-spec.ts @@ -230,12 +230,12 @@ describe('scan operator', () => { type('should accept array typed reducers', () => { let a: Observable<{ a: number; b: string }>; - a.pipe(reduce<{ a: number; b: string }>((acc, value) => acc.concat(value), [])); + a.pipe(scan((acc, value) => acc.concat(value), [])); }); type('should accept T typed reducers', () => { let a: Observable<{ a?: number; b?: string }>; - a.pipe(reduce((acc, value) => { + a.pipe(scan((acc, value) => { value.a = acc.a; value.b = acc.b; return acc; @@ -244,7 +244,7 @@ describe('scan operator', () => { type('should accept R typed reducers', () => { let a: Observable<{ a: number; b: string }>; - a.pipe(reduce<{ a?: number; b?: string }>((acc, value) => { + a.pipe(scan<{ a?: number; b?: string }>((acc, value) => { value.a = acc.a; value.b = acc.b; return acc; diff --git a/src/internal/operators/reduce.ts b/src/internal/operators/reduce.ts index 2b1c3f8460..6039fdb123 100644 --- a/src/internal/operators/reduce.ts +++ b/src/internal/operators/reduce.ts @@ -6,9 +6,9 @@ import { OperatorFunction, MonoTypeOperatorFunction } from '../types'; import { pipe } from '../util/pipe'; /* tslint:disable:max-line-length */ +export function reduce(accumulator: (acc: R, value: T, index: number) => R, seed: R): OperatorFunction; export function reduce(accumulator: (acc: T, value: T, index: number) => T, seed?: T): MonoTypeOperatorFunction; -export function reduce(accumulator: (acc: T[], value: T, index: number) => T[], seed: T[]): OperatorFunction; -export function reduce(accumulator: (acc: R, value: T, index: number) => R, seed?: R): OperatorFunction; +export function reduce(accumulator: (acc: R, value: T, index: number) => R): OperatorFunction; /* tslint:enable:max-line-length */ /** @@ -62,20 +62,20 @@ export function reduce(accumulator: (acc: R, value: T, index: number) => R * @method reduce * @owner Observable */ -export function reduce(accumulator: (acc: R, value: T, index?: number) => R, seed?: R): OperatorFunction { +export function reduce(accumulator: (acc: T | R, value: T, index?: number) => T | R, seed?: T | R): OperatorFunction { // providing a seed of `undefined` *should* be valid and trigger // hasSeed! so don't use `seed !== undefined` checks! // For this reason, we have to check it here at the original call site // otherwise inside Operator/Subscriber we won't know if `undefined` // means they didn't provide anything or if they literally provided `undefined` if (arguments.length >= 2) { - return function reduceOperatorFunctionWithSeed(source: Observable): Observable { + return function reduceOperatorFunctionWithSeed(source: Observable): Observable { return pipe(scan(accumulator, seed), takeLast(1), defaultIfEmpty(seed))(source); }; } - return function reduceOperatorFunction(source: Observable): Observable { + return function reduceOperatorFunction(source: Observable): Observable { return pipe( - scan((acc: R, value: T, index: number): R => accumulator(acc, value, index + 1)), + scan((acc, value, index) => accumulator(acc, value, index + 1)), takeLast(1), )(source); }; diff --git a/src/internal/operators/scan.ts b/src/internal/operators/scan.ts index f3b4d03470..a1eb511e40 100644 --- a/src/internal/operators/scan.ts +++ b/src/internal/operators/scan.ts @@ -4,9 +4,9 @@ import { Subscriber } from '../Subscriber'; import { OperatorFunction, MonoTypeOperatorFunction } from '../types'; /* tslint:disable:max-line-length */ +export function scan(accumulator: (acc: R, value: T, index: number) => R, seed: R): OperatorFunction; export function scan(accumulator: (acc: T, value: T, index: number) => T, seed?: T): MonoTypeOperatorFunction; -export function scan(accumulator: (acc: T[], value: T, index: number) => T[], seed?: T[]): OperatorFunction; -export function scan(accumulator: (acc: R, value: T, index: number) => R, seed?: R): OperatorFunction; +export function scan(accumulator: (acc: R, value: T, index: number) => R): OperatorFunction; /* tslint:enable:max-line-length */ /**