Skip to content

Commit ec00390

Browse files
Update: propEq to allow wider-typing for value in comparison (#74)
1 parent f05db5a commit ec00390

6 files changed

Lines changed: 318 additions & 47 deletions

File tree

test/allPass.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,21 @@ expectError(
4848
nickname: 'Blade'
4949
})
5050
);
51+
52+
const isQueen = propEq('Q', 'rank');
53+
const isSpade = propEq('♠︎', 'suit');
54+
const isQueenOfSpades = allPass([isQueen, isSpade]);
55+
56+
isQueenOfSpades({
57+
rank: '2',
58+
suit: '♠︎'
59+
});
60+
61+
const isQueen2 = (x: Record<'rank', string>) => x.rank === 'Q';
62+
const isSpade2 = (x: Record<'suit', string>) => x.suit === '♠︎';
63+
const isQueenOfSpades2 = allPass([isQueen2, isSpade2]);
64+
65+
isQueenOfSpades2({
66+
rank: '2',
67+
suit: '♠︎'
68+
});

test/anyPass.test.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ expectType<boolean>(
2525
})
2626
);
2727

28-
expectError(
28+
expectType<boolean>(
2929
isVampire({
3030
age: 21,
3131
garlic_allergy: true,
3232
sun_allergy: true,
33-
fast: false,
34-
fear: true
33+
fast: null,
34+
fear: undefined
3535
})
3636
);
3737

@@ -48,3 +48,26 @@ expectError(
4848
nickname: 'Blade'
4949
})
5050
);
51+
52+
const isQueen = propEq('Q', 'rank');
53+
const isSpade = propEq('♠︎', 'suit');
54+
const isQueenOfSpades = anyPass([isQueen, isSpade]);
55+
56+
expectType<boolean>(isQueenOfSpades({
57+
rank: '2',
58+
suit: '♠︎'
59+
}));
60+
61+
expectError(isQueenOfSpades({
62+
rank: 2,
63+
suit: '♠︎'
64+
}));
65+
66+
const isQueen2 = (x: Record<'rank', string>) => x.rank === 'Q';
67+
const isSpade2 = (x: Record<'suit', string>) => x.suit === '♠︎';
68+
const isQueenOfSpades2 = anyPass([isQueen2, isSpade2]);
69+
70+
isQueenOfSpades2({
71+
rank: '2',
72+
suit: '♠︎'
73+
});

test/propEq.test.ts

Lines changed: 148 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,153 @@ import { expectError, expectType } from 'tsd';
33
import { propEq } from '../es';
44

55
type Obj = {
6-
union: 'foo' | 'bar';
7-
str: string;
8-
num: number;
9-
u: undefined;
10-
n: null;
6+
literals: 'A' | 'B';
7+
unions: number | string;
8+
nullable: number | null | undefined;
9+
optional?: number;
1110
};
1211

13-
// propEq(val, name, obj)
14-
expectType<boolean>(propEq('foo', 'union', {} as Obj));
15-
// non-union string fails
16-
expectError(propEq('nope', 'union', {} as Obj));
17-
// completely different type fails
18-
expectError(propEq(2, 'union', {} as Obj));
19-
20-
// propEq(val)(name)(obj)
21-
expectType<boolean>(propEq('foo')('union')({} as Obj));
22-
// 'nope' is inferred as 'string' here.
23-
expectType<boolean>(propEq('nope')('union')({} as Obj));
24-
// completely different type fails
25-
expectError(propEq(2)('union')({} as Obj));
26-
27-
// propEq(val)(name), obj)
28-
expectType<boolean>(propEq('foo')('union', {} as Obj));
29-
// 'nope' is inferred as 'string' here.
30-
expectType<boolean>(propEq('nope')('union', {} as Obj));
31-
// completely different type fails
32-
expectError(propEq(2)('union', {} as Obj));
33-
34-
// propEq(val, name)(obj)
35-
expectType<boolean>(propEq('foo', 'union')({} as Obj));
36-
// 'nope' is inferred as 'string' here.
37-
expectType<boolean>(propEq('nope', 'union')({} as Obj));
38-
// completely different type fails
39-
expectError(propEq(2, 'union')({} as Obj));
12+
const obj = {} as Obj;
13+
14+
//
15+
// literals
16+
//
17+
18+
// happy path works as expected
19+
expectType<boolean>(propEq('A')('literals')(obj));
20+
expectType<boolean>(propEq('A', 'literals')(obj));
21+
expectType<boolean>(propEq('A', 'literals', obj));
22+
23+
// accepts any type that obj[key] can be widened too
24+
expectType<boolean>(propEq('C')('literals')(obj));
25+
expectType<boolean>(propEq('C', 'literals')(obj));
26+
// only propEq(val, key, obj) requests non-widened types
27+
expectError(propEq('C', 'literals', obj));
28+
29+
// rejects if type cannot be widened too
30+
expectError(propEq(2)('literals')(obj));
31+
expectError(propEq(2, 'literals')(obj));
32+
expectError(propEq(2, 'literals', obj));
33+
34+
// manually widened also works
35+
expectType<boolean>(propEq('A' as string)('literals')(obj));
36+
expectType<boolean>(propEq('A' as string, 'literals')(obj));
37+
// only rejects for propEq(val, key, obj), `string` is too wide for 'A' | 'B'
38+
expectError(propEq('A' as string, 'literals', obj));
39+
40+
// rejects if key is not on obj
41+
expectError(propEq('A')('literals')({} as Omit<Obj, 'literals'>));
42+
expectError(propEq('A', 'literals')({} as Omit<Obj, 'literals'>));
43+
expectError(propEq('A', 'literals', {} as Omit<Obj, 'literals'>));
44+
45+
// rejects empty object literal
46+
expectError(propEq('A')('literals')({}));
47+
expectError(propEq('A', 'literals')({}));
48+
expectError(propEq('A', 'literals', {}));
49+
50+
//
51+
// unions
52+
//
53+
54+
// happy path works as expected
55+
expectType<boolean>(propEq('1')('unions')(obj));
56+
expectType<boolean>(propEq('1', 'unions')(obj));
57+
expectType<boolean>(propEq('1', 'unions', obj));
58+
59+
expectType<boolean>(propEq(1)('unions')(obj));
60+
expectType<boolean>(propEq(1, 'unions')(obj));
61+
expectType<boolean>(propEq(1, 'unions', obj));
62+
63+
// rejects if typeof val not part of union type
64+
expectError(propEq(true)('unions')(obj));
65+
expectError(propEq(true, 'unions')(obj));
66+
expectError(propEq(true, 'unions', obj));
67+
68+
// rejects if key is not on obj
69+
expectError(propEq('1')('unions')({} as Omit<Obj, 'unions'>));
70+
expectError(propEq('1', 'unions')({} as Omit<Obj, 'unions'>));
71+
expectError(propEq('1', 'unions', {} as Omit<Obj, 'unions'>));
72+
73+
// rejects empty object literal
74+
expectError(propEq('1')('unions')({}));
75+
expectError(propEq('1', 'unions')({}));
76+
expectError(propEq('1', 'unions', {}));
77+
78+
//
79+
// nullable
80+
//
81+
82+
// happy path works as expected
83+
expectType<boolean>(propEq(1)('nullable')(obj));
84+
expectType<boolean>(propEq(1, 'nullable')(obj));
85+
expectType<boolean>(propEq(1, 'nullable', obj));
86+
87+
expectType<boolean>(propEq(null)('nullable')(obj));
88+
expectType<boolean>(propEq(null, 'nullable')(obj));
89+
expectType<boolean>(propEq(null, 'nullable', obj));
90+
91+
expectType<boolean>(propEq(undefined)('nullable')(obj));
92+
expectType<boolean>(propEq(undefined, 'nullable')(obj));
93+
expectType<boolean>(propEq(undefined, 'nullable', obj));
94+
95+
// rejects if typeof val not part of union type
96+
expectError(propEq(true)('nullable')(obj));
97+
expectError(propEq(true, 'nullable')(obj));
98+
expectError(propEq(true, 'nullable', obj));
99+
100+
// rejects if key is not on obj
101+
expectError(propEq(1)('nullable')({} as Omit<Obj, 'nullable'>));
102+
expectError(propEq(1, 'nullable')({} as Omit<Obj, 'nullable'>));
103+
expectError(propEq(1, 'nullable', {} as Omit<Obj, 'nullable'>));
104+
105+
// rejects empty object literal
106+
expectError(propEq(1)('nullable')({}));
107+
expectError(propEq(1, 'nullable')({}));
108+
expectError(propEq(1, 'nullable', {}));
109+
110+
//
111+
// optional
112+
//
113+
114+
// happy path works as expected
115+
expectType<boolean>(propEq(1)('optional')(obj));
116+
expectType<boolean>(propEq(1, 'optional')(obj));
117+
expectType<boolean>(propEq(1, 'optional', obj));
118+
119+
expectType<boolean>(propEq(undefined)('optional')(obj));
120+
expectType<boolean>(propEq(undefined, 'optional')(obj));
121+
expectType<boolean>(propEq(undefined, 'optional', obj));
122+
123+
// `null` produces error for `optional`. this is expected because typescript strictNullCheck `null !== undefined`
124+
expectError(propEq(null)('optional')(obj));
125+
expectError(propEq(null, 'optional')(obj));
126+
expectError(propEq(null, 'optional', obj));
127+
128+
// rejects if typeof val not part of union type
129+
expectError(propEq(true)('optional')(obj));
130+
expectError(propEq(true, 'optional')(obj));
131+
expectError(propEq(true, 'optional', obj));
132+
133+
// rejects if key is not on obj
134+
expectError(propEq(1)('optional')({} as Omit<Obj, 'optional'>));
135+
expectError(propEq(1, 'optional')({} as Omit<Obj, 'optional'>));
136+
expectError(propEq(1, 'optional', {} as Omit<Obj, 'optional'>));
137+
138+
// rejects empty object literal literal
139+
expectError(propEq(1)('optional')({}));
140+
expectError(propEq(1, 'optional')({}));
141+
expectError(propEq(1, 'optional', {}));
142+
143+
//
144+
// other non-happy paths
145+
//
146+
147+
// rejects unknown key
148+
expectError(propEq(1)('whatever')(obj));
149+
expectError(propEq(1, 'whatever')(obj));
150+
expectError(propEq(1, 'whatever', obj));
151+
152+
// rejects unknown key on emptyu object literal
153+
expectError(propEq(1)('whatever')({}));
154+
expectError(propEq(1, 'whatever')({}));
155+
expectError(propEq(1, 'whatever', {}));

types/allPass.d.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
1+
// narrowing
12
export function allPass<T, TF1 extends T, TF2 extends T>(
2-
predicates: [(a: T) => a is TF1, (a: T) => a is TF2]
3+
predicates: [
4+
(a: T) => a is TF1,
5+
(a: T) => a is TF2
6+
]
37
): (a: T) => a is TF1 & TF2;
48
export function allPass<T, TF1 extends T, TF2 extends T, TF3 extends T>(
5-
predicates: [(a: T) => a is TF1, (a: T) => a is TF2, (a: T) => a is TF3],
9+
predicates: [
10+
(a: T) => a is TF1,
11+
(a: T) => a is TF2,
12+
(a: T) => a is TF3
13+
],
614
): (a: T) => a is TF1 & TF2 & TF3;
715
export function allPass<T, TF1 extends T, TF2 extends T, TF3 extends T, TF4 extends T>(
8-
predicates: [(a: T) => a is TF1, (a: T) => a is TF2, (a: T) => a is TF3, (a: T) => a is TF4],
16+
predicates: [
17+
(a: T) => a is TF1,
18+
(a: T) => a is TF2,
19+
(a: T) => a is TF3,
20+
(a: T) => a is TF4
21+
],
922
): (a: T) => a is TF1 & TF2 & TF3 & TF4;
1023
export function allPass<T, TF1 extends T, TF2 extends T, TF3 extends T, TF4 extends T, TF5 extends T>(
1124
predicates: [
@@ -26,4 +39,46 @@ export function allPass<T, TF1 extends T, TF2 extends T, TF3 extends T, TF4 exte
2639
(a: T) => a is TF6
2740
],
2841
): (a: T) => a is TF1 & TF2 & TF3 & TF4 & TF5 & TF6;
42+
// regular
43+
export function allPass<T1, T2>(
44+
predicates: [
45+
(a: T1) => boolean,
46+
(a: T2) => boolean
47+
],
48+
): (a: T1 & T2) => boolean;
49+
export function allPass<T1, T2, T3>(
50+
predicates: [
51+
(a: T1) => boolean,
52+
(a: T2) => boolean,
53+
(a: T3) => boolean
54+
],
55+
): (a: T1 & T2 & T3) => boolean;
56+
export function allPass<T1, T2, T3, T4>(
57+
predicates: [
58+
(a: T1) => boolean,
59+
(a: T2) => boolean,
60+
(a: T3) => boolean,
61+
(a: T4) => boolean
62+
],
63+
): (a: T1 & T2 & T3 & T4) => boolean;
64+
export function allPass<T1, T2, T3, T4, T5>(
65+
predicates: [
66+
(a: T1) => boolean,
67+
(a: T2) => boolean,
68+
(a: T3) => boolean,
69+
(a: T4) => boolean,
70+
(a: T5) => boolean
71+
],
72+
): (a: T1 & T2 & T3 & T4 & T5) => boolean;
73+
export function allPass<T1, T2, T3, T4, T5, T6>(
74+
predicates: [
75+
(a: T1) => boolean,
76+
(a: T2) => boolean,
77+
(a: T3) => boolean,
78+
(a: T4) => boolean,
79+
(a: T5) => boolean,
80+
(a: T6) => boolean
81+
],
82+
): (a: T1 & T2 & T3 & T4 & T5 & T6) => boolean;
83+
// catch-all
2984
export function allPass<F extends (...args: any[]) => boolean>(predicates: readonly F[]): F;

0 commit comments

Comments
 (0)