Skip to content

Commit

Permalink
refactor: Predicate types (#5842)
Browse files Browse the repository at this point in the history
* refactor: Predicate types

Update operators with predicates to properly handle typings with `Boolean` constructor and predicates that return either always `true` or always `false`, depending on the operator.

- Updates dtslint tests.

* chore: update golden files

* refactor(takeWhile): ensure proper types when inclusive

* refactor(every): Improve types, address comments

- Every now infers correctly when `Boolean` constructor is used.

* refactor(takeWhile): fix types for false predicate and inclusive
  • Loading branch information
benlesh authored Nov 10, 2020
1 parent 0186e8a commit b736182
Show file tree
Hide file tree
Showing 23 changed files with 277 additions and 86 deletions.
10 changes: 6 additions & 4 deletions api_guard/dist/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ export interface ErrorObserver<T> {

export declare type FactoryOrValue<T> = T | (() => T);

export declare type Falsy = null | undefined | false | 0 | -0 | 0n | '';

export declare function firstValueFrom<T>(source: Observable<T>): Promise<T>;

export declare function forkJoin(sources: []): Observable<never>;
Expand Down Expand Up @@ -371,9 +373,7 @@ export declare function of<T, T2, T3, T4, T5, T6, T7, T8, T9>(a: T, b: T2, c: T3
export declare function of(): Observable<never>;
export declare function of<T>(): Observable<T>;
export declare function of<T>(value: T): Observable<T>;
export declare function of<T, U>(value1: T, value2: U): Observable<T | U>;
export declare function of<T, U, V>(value1: T, value2: U, value3: V): Observable<T | U | V>;
export declare function of<A extends Array<any>>(...args: A): Observable<ValueFromArray<A>>;
export declare function of<A extends readonly unknown[]>(...args: A): Observable<ValueFromArray<A>>;

export declare function onErrorResumeNext(): Observable<never>;
export declare function onErrorResumeNext<O extends ObservableInput<any>>(arrayOfSources: O[]): Observable<ObservedValueOf<O>>;
Expand Down Expand Up @@ -533,6 +533,8 @@ export interface TimestampProvider {
now(): number;
}

export declare type TruthyTypesOf<T> = T extends Falsy ? never : T;

export interface UnaryFunction<T, R> {
(source: T): R;
}
Expand All @@ -549,7 +551,7 @@ export declare const UnsubscriptionError: UnsubscriptionErrorCtor;

export declare function using<T>(resourceFactory: () => Unsubscribable | void, observableFactory: (resource: Unsubscribable | void) => ObservableInput<T> | void): Observable<T>;

export declare type ValueFromArray<A> = A extends Array<infer T> ? T : never;
export declare type ValueFromArray<A extends readonly unknown[]> = A extends Array<infer T> ? T : never;

export declare type ValueFromNotification<T> = T extends {
kind: 'N' | 'E' | 'C';
Expand Down
25 changes: 24 additions & 1 deletion api_guard/dist/types/operators/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export declare function endWith<T, A, B, C, D, E>(v1: A, v2: B, v3: C, v4: D, v5
export declare function endWith<T, A, B, C, D, E, F>(v1: A, v2: B, v3: C, v4: D, v5: E, v6: F, scheduler: SchedulerLike): OperatorFunction<T, T | A | B | C | D | E | F>;
export declare function endWith<T, A extends any[] = T[]>(...args: A): OperatorFunction<T, T | ValueFromArray<A>>;

export declare function every<T>(predicate: BooleanConstructor, thisArg?: any): OperatorFunction<T, Exclude<T, Falsy> extends never ? false : boolean>;
export declare function every<T>(predicate: (value: T, index: number, source: Observable<T>) => boolean, thisArg?: any): OperatorFunction<T, boolean>;

export declare function exhaust<T>(): OperatorFunction<ObservableInput<T>, T>;
Expand All @@ -108,19 +109,28 @@ export declare function exhaustMap<T, I, R>(project: (value: T, index: number) =
export declare function expand<T, R>(project: (value: T, index: number) => ObservableInput<R>, concurrent?: number, scheduler?: SchedulerLike): OperatorFunction<T, R>;
export declare function expand<T, R>(project: (value: T, index: number) => ObservableInput<R>, concurrent: number | undefined, scheduler: SchedulerLike): OperatorFunction<T, R>;

export declare function filter<T>(predicate: (value: T, index: number) => false, thisArg?: any): OperatorFunction<T, never>;
export declare function filter<T, S extends T>(predicate: (value: T, index: number) => value is S, thisArg?: any): OperatorFunction<T, S>;
export declare function filter<T>(predicate: BooleanConstructor): OperatorFunction<T, T extends null | undefined | false | 0 | -0 | 0n | '' ? never : T>;
export declare function filter<T>(predicate: BooleanConstructor): OperatorFunction<T, TruthyTypesOf<T>>;
export declare function filter<T>(predicate: (value: T, index: number) => boolean, thisArg?: any): MonoTypeOperatorFunction<T>;

export declare function finalize<T>(callback: () => void): MonoTypeOperatorFunction<T>;

export declare function find<T>(predicate: BooleanConstructor): OperatorFunction<T, TruthyTypesOf<T>>;
export declare function find<T, S extends T>(predicate: (value: T, index: number, source: Observable<T>) => value is S, thisArg?: any): OperatorFunction<T, S | undefined>;
export declare function find<T>(predicate: (value: T, index: number, source: Observable<T>) => boolean, thisArg?: any): OperatorFunction<T, T | undefined>;

export declare function findIndex<T>(predicate: (value: T, index: number, source: Observable<T>) => false, thisArg?: any): OperatorFunction<T, -1>;
export declare function findIndex<T>(predicate: BooleanConstructor, thisArg?: any): OperatorFunction<T, T extends Falsy ? -1 : number>;
export declare function findIndex<T>(predicate: (value: T, index: number, source: Observable<T>) => boolean, thisArg?: any): OperatorFunction<T, number>;

export declare function first<T, D = T>(predicate?: null, defaultValue?: D): OperatorFunction<T, T | D>;
export declare function first<T>(predicate: BooleanConstructor): OperatorFunction<T, TruthyTypesOf<T>>;
export declare function first<T, D>(predicate: BooleanConstructor, defaultValue: D): OperatorFunction<T, TruthyTypesOf<T> | D>;
export declare function first<T, S extends T>(predicate: (value: T, index: number, source: Observable<T>) => value is S, defaultValue?: S): OperatorFunction<T, S>;
export declare function first<T, S extends T, D>(predicate: (value: T, index: number, source: Observable<T>) => value is S, defaultValue: D): OperatorFunction<T, S | D>;
export declare function first<T, D>(predicate: (value: T, index: number, source: Observable<T>) => false, defaultValue: D): OperatorFunction<T, D>;
export declare function first<T>(predicate: (value: T, index: number, source: Observable<T>) => false): OperatorFunction<T, never>;
export declare function first<T, D = T>(predicate: (value: T, index: number, source: Observable<T>) => boolean, defaultValue?: D): OperatorFunction<T, T | D>;

export declare const flatMap: typeof mergeMap;
Expand All @@ -135,8 +145,12 @@ export declare function ignoreElements(): OperatorFunction<any, never>;

export declare function isEmpty<T>(): OperatorFunction<T, boolean>;

export declare function last<T>(predicate: BooleanConstructor): OperatorFunction<T, TruthyTypesOf<T>>;
export declare function last<T, D>(predicate: BooleanConstructor, defaultValue: D): OperatorFunction<T, TruthyTypesOf<T> | D>;
export declare function last<T, D = T>(predicate?: null, defaultValue?: D): OperatorFunction<T, T | D>;
export declare function last<T, S extends T>(predicate: (value: T, index: number, source: Observable<T>) => value is S, defaultValue?: S): OperatorFunction<T, S>;
export declare function last<T, D>(predicate: (value: T, index: number, source: Observable<T>) => false, defaultValue: D): OperatorFunction<T, D>;
export declare function last<T>(predicate: (value: T, index: number, source: Observable<T>) => false): OperatorFunction<T, never>;
export declare function last<T, D = T>(predicate: (value: T, index: number, source: Observable<T>) => boolean, defaultValue?: D): OperatorFunction<T, T | D>;

export declare function map<T, R>(project: (value: T, index: number) => R, thisArg?: any): OperatorFunction<T, R>;
Expand Down Expand Up @@ -240,6 +254,8 @@ export declare function share<T>(): MonoTypeOperatorFunction<T>;
export declare function shareReplay<T>(config: ShareReplayConfig): MonoTypeOperatorFunction<T>;
export declare function shareReplay<T>(bufferSize?: number, windowTime?: number, scheduler?: SchedulerLike): MonoTypeOperatorFunction<T>;

export declare function single<T>(predicate: BooleanConstructor): OperatorFunction<T, TruthyTypesOf<T>>;
export declare function single<T>(predicate: (value: T, index: number, source: Observable<T>) => false): OperatorFunction<T, never>;
export declare function single<T>(predicate?: (value: T, index: number, source: Observable<T>) => boolean): MonoTypeOperatorFunction<T>;

export declare function skip<T>(count: number): MonoTypeOperatorFunction<T>;
Expand All @@ -248,6 +264,8 @@ export declare function skipLast<T>(skipCount: number): MonoTypeOperatorFunction

export declare function skipUntil<T>(notifier: Observable<any>): MonoTypeOperatorFunction<T>;

export declare function skipWhile<T>(predicate: BooleanConstructor): OperatorFunction<T, Extract<T, Falsy> extends never ? never : T>;
export declare function skipWhile<T>(predicate: (value: T, index: number) => true): OperatorFunction<T, never>;
export declare function skipWhile<T>(predicate: (value: T, index: number) => boolean): MonoTypeOperatorFunction<T>;

export declare function startWith<T>(scheduler: SchedulerLike): MonoTypeOperatorFunction<T>;
Expand Down Expand Up @@ -279,6 +297,11 @@ export declare function takeLast<T>(count: number): MonoTypeOperatorFunction<T>;

export declare function takeUntil<T>(notifier: ObservableInput<any>): MonoTypeOperatorFunction<T>;

export declare function takeWhile<T>(predicate: (value: T, index: number) => false, inclusive: true): MonoTypeOperatorFunction<T>;
export declare function takeWhile<T>(predicate: (value: T, index: number) => false, inclusive?: false): OperatorFunction<T, never>;
export declare function takeWhile<T>(predicate: BooleanConstructor): OperatorFunction<T, Exclude<T, Falsy> extends never ? never : T>;
export declare function takeWhile<T>(predicate: BooleanConstructor, inclusive: false): OperatorFunction<T, Exclude<T, Falsy> extends never ? never : T>;
export declare function takeWhile<T>(predicate: BooleanConstructor, inclusive: true): MonoTypeOperatorFunction<T>;
export declare function takeWhile<T, S extends T>(predicate: (value: T, index: number) => value is S): OperatorFunction<T, S>;
export declare function takeWhile<T, S extends T>(predicate: (value: T, index: number) => value is S, inclusive: false): OperatorFunction<T, S>;
export declare function takeWhile<T>(predicate: (value: T, index: number) => boolean, inclusive?: boolean): MonoTypeOperatorFunction<T>;
Expand Down
2 changes: 1 addition & 1 deletion spec-dtslint/observables/of-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,4 @@ it('should deprecate correctly', () => {
of(a, b); // $ExpectNoDeprecation
of(a, b, c); // $ExpectNoDeprecation
of(a, b, c, d); // $ExpectNoDeprecation
});
});
8 changes: 8 additions & 0 deletions spec-dtslint/operators/every-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,11 @@ it('should enforce index type of number', () => {
it('should expect function parameter', () => {
const a = of(1, 2, 3).pipe(every(9)); // $ExpectError
});

it('should handle the Boolean constructor', () => {
const a = of(0 as const, '' as const, false as const, null, undefined, -0 as const, 0n as const).pipe(every(Boolean)); // $ExpectType Observable<false>
const b = of(0 as const, '' as const, 'hi there' as const, false as const, null, undefined, -0 as const, 0n as const).pipe(every(Boolean)); // $ExpectType Observable<boolean>
const c = of('test' as const, true as const, 1 as const, [], {}).pipe(every(Boolean)); // $ExpectType Observable<boolean>
const d = of(NaN, NaN, NaN).pipe(every(Boolean)); // $ExpectType Observable<boolean>
const e = of(0, 1, 0).pipe(every(Boolean)); // $ExpectType Observable<boolean>
})
6 changes: 6 additions & 0 deletions spec-dtslint/operators/filter-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ it('should support Boolean as a predicate', () => {
const u = of(0 as const, -0 as const).pipe(filter(Boolean)); // $ExpectType Observable<never>
const v = of('' as const, "foo" as const, "bar" as const).pipe(filter(Boolean)); // $ExpectType Observable<"foo" | "bar">
const w = of('' as const).pipe(filter(Boolean)); // $ExpectType Observable<never>
// Intentionally weird looking test... `false` is `boolean`, which is `true | false`.
const x = of(false, false, false, false).pipe(filter(Boolean)); // $ExpectType Observable<true>
});

it('should narrow on always-false predicates', () => {
const o = of(1, 2, 3).pipe(filter(() => false)); // $ExpectType Observable<never>
});

// I've not been able to effect a failing dtslint test for this situation and a
Expand Down
9 changes: 9 additions & 0 deletions spec-dtslint/operators/find-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,12 @@ it('should support a predicate that takes an index', () => {
it('should support a predicate that takes an index and the source', () => {
const o = of('foo').pipe(find((s, index, source) => true)); // $ExpectType Observable<string | undefined>
});

it('should support Boolean properly', () => {
const o1 = of('' as const).pipe(find(Boolean)); // $ExpectType Observable<never>
const o2 = of('' as const, 'hi' as const).pipe(find(Boolean)); // $ExpectType Observable<"hi">
const o3 = of('' as const, 0 as const, 'test' as const, 'what' as const).pipe(find(Boolean)); // $ExpectType Observable<"test" | "what">
const o5 = of(false as const, null, undefined, '' as const, 0 as const, 0 as const).pipe(find(Boolean)); // $ExpectType Observable<never>
// Intentionally weird looking: Because `Observable<boolean>` is `Observable<true | false>` and `true` is the truthy bit.
const o4 = of(false, false, false, false).pipe(find(Boolean)); // $ExpectType Observable<true>
});
9 changes: 9 additions & 0 deletions spec-dtslint/operators/findIndex-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,12 @@ it('should enforce predicate types', () => {
it('should enforce predicate return type', () => {
const o = of('foo', 'bar', 'baz').pipe(findIndex(p => p)); // $ExpectError
});

it('should support Boolean constructor', () => {
const a = of(0 as const, -0 as const, null, undefined, false as const, '' as const).pipe(findIndex(Boolean)); // $ExpectType Observable<-1>
const b = of(0 as const, -0 as const, null, 'hi there' as const, undefined, false as const, '' as const).pipe(findIndex(Boolean)); // $ExpectType Observable<number>
});

it('should properly narrow an always false predicate', () => {
const a = of('foo', 'bar', 'baz').pipe(findIndex(() => false)); // $ExpectType Observable<-1>
})
12 changes: 8 additions & 4 deletions spec-dtslint/operators/first-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ it('should support a user-defined type guard with an S default', () => {
});

it('should widen a user-defined type guard with a non-S default', () => {
const o = of('foo').pipe(first(isFooBar, false)); // $ExpectType Observable<string | boolean>
const o = of('foo').pipe(first(isFooBar, false)); // $ExpectType Observable<boolean | "foo" | "bar">
});

it('should support a predicate with no default', () => {
Expand All @@ -59,6 +59,10 @@ it('should support a predicate with a non-T default', () => {
const o = of('foo').pipe(first(x => !!x, false)); // $ExpectType Observable<string | boolean>
});

it('should default D to T with a predicate', () => {
const o = of('foo').pipe(first<string>(x => !!x)); // $Observable<string>
});
it('should work properly with the Boolean constructor', () => {
const o1 = of('' as const).pipe(first(Boolean)); // $ExpectType Observable<never>
const o2 = of('', 'hi').pipe(first(Boolean)); // $ExpectType Observable<string>
const o3 = of('' as const, 'hi' as const).pipe(first(Boolean)); // $ExpectType Observable<"hi">
const o4 = of(0 as const, 'hi' as const).pipe(first(Boolean)); // $ExpectType Observable<"hi">
const o5 = of(0 as const, 'hi' as const, 'what' as const).pipe(first(Boolean)); // $ExpectType Observable<"hi" | "what">
})
14 changes: 13 additions & 1 deletion spec-dtslint/operators/last-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,17 @@ it('should support a predicate with a non-T default', () => {
});

it('should default D to T with a predicate', () => {
const o = of('foo').pipe(last<string>(x => !!x)); // $Observable<string>
const o = of('foo').pipe(last<string>(x => !!x)); // $ExpectType Observable<string>
});

it('should handle predicates that always return false properly', () => {
const a = of('foo', 'bar').pipe(last(() => false as const)); // $ExpectType Observable<never>
const b = of('foo', 'bar').pipe(last(() => false as const, 1337 as const)); // $ExpectType Observable<1337>
});

it('should handle Boolean constructor properly', () => {
const a = of(0 as const, -0 as const, null, undefined, false as const, '' as const, 0n as const).pipe(last(Boolean)); // $ExpectType Observable<never>
const b = of(0 as const, -0 as const, null, undefined, false as const, '' as const, 0n as const).pipe(last(Boolean, 'test' as const)); // $ExpectType Observable<"test">
const c = of(0 as const, -0 as const, null, 'hi' as const, undefined, false as const, '' as const, 0n as const).pipe(last(Boolean)); // $ExpectType Observable<"hi">
const d = of(0 as const, -0 as const, null, 'hi' as const, undefined, false as const, '' as const, 0n as const).pipe(last(Boolean, 'test' as const)); // $ExpectType Observable<"test" | "hi">
})
9 changes: 9 additions & 0 deletions spec-dtslint/operators/single-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,12 @@ it('should enforce index type', () => {
it('should enforce source type', () => {
const o = of('foo').pipe(single(((value, index, source: Observable<number>) => value === 'foo'))); // $ExpectError
});

it('should handle Boolean constructor properly', () => {
const a = of(null, undefined, 0 as const, -0 as const, 0n as const, '' as const).pipe(single(Boolean)); // $ExpectType Observable<never>
const b = of(null, undefined, 0 as const, 'test' as const, -0 as const, 0n as const, '' as const).pipe(single(Boolean)); // $ExpectType Observable<"test">
});

it('should handle predicates that always return false properly', () => {
const a = of(1, 2, 3, 4).pipe(single(() => false as const)); // $ExpectType Observable<never>
});
12 changes: 12 additions & 0 deletions spec-dtslint/operators/skipWhile-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,15 @@ it('should enforce predicate types', () => {
it('should enforce predicate return type', () => {
const o = of('foo', 'bar', 'baz').pipe(skipWhile(value => value)); // $ExpectError
});

it('should handle Boolean constructor properly', () => {
// this one is a bit odd, but probably okay.
const a = of(null, undefined, 0 as const, -0 as const, '' as const, 0n as const, false as const).pipe(skipWhile(Boolean)); // $ExpectType Observable<Falsy>
const b = of(null, 0 as const, -0 as const, '' as const, 0n as const, false as const).pipe(skipWhile(Boolean)); // $ExpectType Observable<false | "" | 0 | 0n | null>
const c = of(1, 2, 3, '' as const, 0n as const, false as const, 4).pipe(skipWhile(Boolean)) // $ExpectType Observable<number | false | "" | 0n>
const d = of(true as const, 123 as const, 'HI' as const, {}, []).pipe(skipWhile(Boolean)); // $ExpectType Observable<never>
});

it('should handle predicates that always return true properly', () => {
const a = of(1, 2, 3, 4).pipe(skipWhile(() => true as const)); // $ExpectType Observable<never>
});
14 changes: 14 additions & 0 deletions spec-dtslint/operators/takeWhile-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,17 @@ it('should support a predicate', () => {
it('should support a predicate with inclusive option', () => {
const o = of('foo').pipe(takeWhile(s => true, true)); // $ExpectType Observable<string>
});

it('should properly support Boolean constructor', () => {
const a = of(false as const, 0 as const, -0 as const, 0n as const, '' as const, null, undefined).pipe(takeWhile(Boolean)); // $ExpectType Observable<never>
// This is a weird one... but `Falsy` is equivalent here. I think this is TS trying to be "nice"?
const b = of(false as const, 0 as const, -0 as const, 0n as const, '' as const, null, undefined).pipe(takeWhile(Boolean, true)); // $ExpectType Observable<Falsy>
const c = of(false as const, 0 as const, 'hi' as const, -0 as const, 0n as const, '' as const, null, undefined).pipe(takeWhile(Boolean)); // $ExpectType Observable<false | "" | 0 | 0n | "hi" | null | undefined>
const d = of(false as const, 0 as const, 'hi' as const, -0 as const, 0n as const, '' as const, null, undefined).pipe(takeWhile(Boolean, true)); // $ExpectType Observable<false | "" | 0 | 0n | "hi" | null | undefined>
const e = of(1, ['hi'], false as const, 0 as const, -0 as const, 0n as const, '' as const, null, undefined).pipe(takeWhile(Boolean, true)); // $ExpectType Observable<number | false | "" | 0n | string[] | null | undefined>
});

it('should properly handle predicates that always return false', () => {
const a = of(1, 2, 3).pipe(takeWhile(() => false as const)); // $ExpectType Observable<never>
const b = of(1, 2, 3).pipe(takeWhile(() => false as const, true)); // $ExpectType Observable<number>
});
Loading

0 comments on commit b736182

Please sign in to comment.