diff --git a/test/allPass.test.ts b/test/allPass.test.ts index 37040dd..1e3ee1c 100644 --- a/test/allPass.test.ts +++ b/test/allPass.test.ts @@ -48,3 +48,21 @@ expectError( nickname: 'Blade' }) ); + +const isQueen = propEq('Q', 'rank'); +const isSpade = propEq('♠︎', 'suit'); +const isQueenOfSpades = allPass([isQueen, isSpade]); + +isQueenOfSpades({ + rank: '2', + suit: '♠︎' +}); + +const isQueen2 = (x: Record<'rank', string>) => x.rank === 'Q'; +const isSpade2 = (x: Record<'suit', string>) => x.suit === '♠︎'; +const isQueenOfSpades2 = allPass([isQueen2, isSpade2]); + +isQueenOfSpades2({ + rank: '2', + suit: '♠︎' +}); diff --git a/test/anyPass.test.ts b/test/anyPass.test.ts index ff46aa5..e992387 100644 --- a/test/anyPass.test.ts +++ b/test/anyPass.test.ts @@ -25,13 +25,13 @@ expectType( }) ); -expectError( +expectType( isVampire({ age: 21, garlic_allergy: true, sun_allergy: true, - fast: false, - fear: true + fast: null, + fear: undefined }) ); @@ -48,3 +48,26 @@ expectError( nickname: 'Blade' }) ); + +const isQueen = propEq('Q', 'rank'); +const isSpade = propEq('♠︎', 'suit'); +const isQueenOfSpades = anyPass([isQueen, isSpade]); + +expectType(isQueenOfSpades({ + rank: '2', + suit: '♠︎' +})); + +expectError(isQueenOfSpades({ + rank: 2, + suit: '♠︎' +})); + +const isQueen2 = (x: Record<'rank', string>) => x.rank === 'Q'; +const isSpade2 = (x: Record<'suit', string>) => x.suit === '♠︎'; +const isQueenOfSpades2 = anyPass([isQueen2, isSpade2]); + +isQueenOfSpades2({ + rank: '2', + suit: '♠︎' +}); diff --git a/test/propEq.test.ts b/test/propEq.test.ts index dc5b909..89d70b6 100644 --- a/test/propEq.test.ts +++ b/test/propEq.test.ts @@ -3,37 +3,153 @@ import { expectError, expectType } from 'tsd'; import { propEq } from '../es'; type Obj = { - union: 'foo' | 'bar'; - str: string; - num: number; - u: undefined; - n: null; + literals: 'A' | 'B'; + unions: number | string; + nullable: number | null | undefined; + optional?: number; }; -// propEq(val, name, obj) -expectType(propEq('foo', 'union', {} as Obj)); -// non-union string fails -expectError(propEq('nope', 'union', {} as Obj)); -// completely different type fails -expectError(propEq(2, 'union', {} as Obj)); - -// propEq(val)(name)(obj) -expectType(propEq('foo')('union')({} as Obj)); -// 'nope' is inferred as 'string' here. -expectType(propEq('nope')('union')({} as Obj)); -// completely different type fails -expectError(propEq(2)('union')({} as Obj)); - -// propEq(val)(name), obj) -expectType(propEq('foo')('union', {} as Obj)); -// 'nope' is inferred as 'string' here. -expectType(propEq('nope')('union', {} as Obj)); -// completely different type fails -expectError(propEq(2)('union', {} as Obj)); - -// propEq(val, name)(obj) -expectType(propEq('foo', 'union')({} as Obj)); -// 'nope' is inferred as 'string' here. -expectType(propEq('nope', 'union')({} as Obj)); -// completely different type fails -expectError(propEq(2, 'union')({} as Obj)); +const obj = {} as Obj; + +// +// literals +// + +// happy path works as expected +expectType(propEq('A')('literals')(obj)); +expectType(propEq('A', 'literals')(obj)); +expectType(propEq('A', 'literals', obj)); + +// accepts any type that obj[key] can be widened too +expectType(propEq('C')('literals')(obj)); +expectType(propEq('C', 'literals')(obj)); +// only propEq(val, key, obj) requests non-widened types +expectError(propEq('C', 'literals', obj)); + +// rejects if type cannot be widened too +expectError(propEq(2)('literals')(obj)); +expectError(propEq(2, 'literals')(obj)); +expectError(propEq(2, 'literals', obj)); + +// manually widened also works +expectType(propEq('A' as string)('literals')(obj)); +expectType(propEq('A' as string, 'literals')(obj)); +// only rejects for propEq(val, key, obj), `string` is too wide for 'A' | 'B' +expectError(propEq('A' as string, 'literals', obj)); + +// rejects if key is not on obj +expectError(propEq('A')('literals')({} as Omit)); +expectError(propEq('A', 'literals')({} as Omit)); +expectError(propEq('A', 'literals', {} as Omit)); + +// rejects empty object literal +expectError(propEq('A')('literals')({})); +expectError(propEq('A', 'literals')({})); +expectError(propEq('A', 'literals', {})); + +// +// unions +// + +// happy path works as expected +expectType(propEq('1')('unions')(obj)); +expectType(propEq('1', 'unions')(obj)); +expectType(propEq('1', 'unions', obj)); + +expectType(propEq(1)('unions')(obj)); +expectType(propEq(1, 'unions')(obj)); +expectType(propEq(1, 'unions', obj)); + +// rejects if typeof val not part of union type +expectError(propEq(true)('unions')(obj)); +expectError(propEq(true, 'unions')(obj)); +expectError(propEq(true, 'unions', obj)); + +// rejects if key is not on obj +expectError(propEq('1')('unions')({} as Omit)); +expectError(propEq('1', 'unions')({} as Omit)); +expectError(propEq('1', 'unions', {} as Omit)); + +// rejects empty object literal +expectError(propEq('1')('unions')({})); +expectError(propEq('1', 'unions')({})); +expectError(propEq('1', 'unions', {})); + +// +// nullable +// + +// happy path works as expected +expectType(propEq(1)('nullable')(obj)); +expectType(propEq(1, 'nullable')(obj)); +expectType(propEq(1, 'nullable', obj)); + +expectType(propEq(null)('nullable')(obj)); +expectType(propEq(null, 'nullable')(obj)); +expectType(propEq(null, 'nullable', obj)); + +expectType(propEq(undefined)('nullable')(obj)); +expectType(propEq(undefined, 'nullable')(obj)); +expectType(propEq(undefined, 'nullable', obj)); + +// rejects if typeof val not part of union type +expectError(propEq(true)('nullable')(obj)); +expectError(propEq(true, 'nullable')(obj)); +expectError(propEq(true, 'nullable', obj)); + +// rejects if key is not on obj +expectError(propEq(1)('nullable')({} as Omit)); +expectError(propEq(1, 'nullable')({} as Omit)); +expectError(propEq(1, 'nullable', {} as Omit)); + +// rejects empty object literal +expectError(propEq(1)('nullable')({})); +expectError(propEq(1, 'nullable')({})); +expectError(propEq(1, 'nullable', {})); + +// +// optional +// + +// happy path works as expected +expectType(propEq(1)('optional')(obj)); +expectType(propEq(1, 'optional')(obj)); +expectType(propEq(1, 'optional', obj)); + +expectType(propEq(undefined)('optional')(obj)); +expectType(propEq(undefined, 'optional')(obj)); +expectType(propEq(undefined, 'optional', obj)); + +// `null` produces error for `optional`. this is expected because typescript strictNullCheck `null !== undefined` +expectError(propEq(null)('optional')(obj)); +expectError(propEq(null, 'optional')(obj)); +expectError(propEq(null, 'optional', obj)); + +// rejects if typeof val not part of union type +expectError(propEq(true)('optional')(obj)); +expectError(propEq(true, 'optional')(obj)); +expectError(propEq(true, 'optional', obj)); + +// rejects if key is not on obj +expectError(propEq(1)('optional')({} as Omit)); +expectError(propEq(1, 'optional')({} as Omit)); +expectError(propEq(1, 'optional', {} as Omit)); + +// rejects empty object literal literal +expectError(propEq(1)('optional')({})); +expectError(propEq(1, 'optional')({})); +expectError(propEq(1, 'optional', {})); + +// +// other non-happy paths +// + +// rejects unknown key +expectError(propEq(1)('whatever')(obj)); +expectError(propEq(1, 'whatever')(obj)); +expectError(propEq(1, 'whatever', obj)); + +// rejects unknown key on emptyu object literal +expectError(propEq(1)('whatever')({})); +expectError(propEq(1, 'whatever')({})); +expectError(propEq(1, 'whatever', {})); diff --git a/types/allPass.d.ts b/types/allPass.d.ts index d402738..f3a0cc6 100644 --- a/types/allPass.d.ts +++ b/types/allPass.d.ts @@ -1,11 +1,24 @@ +// narrowing export function allPass( - predicates: [(a: T) => a is TF1, (a: T) => a is TF2] + predicates: [ + (a: T) => a is TF1, + (a: T) => a is TF2 + ] ): (a: T) => a is TF1 & TF2; export function allPass( - predicates: [(a: T) => a is TF1, (a: T) => a is TF2, (a: T) => a is TF3], + predicates: [ + (a: T) => a is TF1, + (a: T) => a is TF2, + (a: T) => a is TF3 + ], ): (a: T) => a is TF1 & TF2 & TF3; export function allPass( - predicates: [(a: T) => a is TF1, (a: T) => a is TF2, (a: T) => a is TF3, (a: T) => a is TF4], + predicates: [ + (a: T) => a is TF1, + (a: T) => a is TF2, + (a: T) => a is TF3, + (a: T) => a is TF4 + ], ): (a: T) => a is TF1 & TF2 & TF3 & TF4; export function allPass( predicates: [ @@ -26,4 +39,46 @@ export function allPass a is TF6 ], ): (a: T) => a is TF1 & TF2 & TF3 & TF4 & TF5 & TF6; +// regular +export function allPass( + predicates: [ + (a: T1) => boolean, + (a: T2) => boolean + ], +): (a: T1 & T2) => boolean; +export function allPass( + predicates: [ + (a: T1) => boolean, + (a: T2) => boolean, + (a: T3) => boolean + ], +): (a: T1 & T2 & T3) => boolean; +export function allPass( + predicates: [ + (a: T1) => boolean, + (a: T2) => boolean, + (a: T3) => boolean, + (a: T4) => boolean + ], +): (a: T1 & T2 & T3 & T4) => boolean; +export function allPass( + predicates: [ + (a: T1) => boolean, + (a: T2) => boolean, + (a: T3) => boolean, + (a: T4) => boolean, + (a: T5) => boolean + ], +): (a: T1 & T2 & T3 & T4 & T5) => boolean; +export function allPass( + predicates: [ + (a: T1) => boolean, + (a: T2) => boolean, + (a: T3) => boolean, + (a: T4) => boolean, + (a: T5) => boolean, + (a: T6) => boolean + ], +): (a: T1 & T2 & T3 & T4 & T5 & T6) => boolean; +// catch-all export function allPass boolean>(predicates: readonly F[]): F; diff --git a/types/anyPass.d.ts b/types/anyPass.d.ts index a056d84..48ca966 100644 --- a/types/anyPass.d.ts +++ b/types/anyPass.d.ts @@ -1,14 +1,24 @@ +// narrowing export function anyPass( - predicates: [(a: T) => a is TF1, (a: T) => a is TF2], + predicates: [ + (a: T) => a is TF1, + (a: T) => a is TF2 + ], ): (a: T) => a is TF1 | TF2; export function anyPass( - predicates: [(a: T) => a is TF1, (a: T) => a is TF2, (a: T) => a is TF3], -): (a: T) => a is TF1 | TF2 | TF3; -export function anyPass( - predicates: [(a: T) => a is TF1, (a: T) => a is TF2, (a: T) => a is TF3], + predicates: [ + (a: T) => a is TF1, + (a: T) => a is TF2, + (a: T) => a is TF3 + ], ): (a: T) => a is TF1 | TF2 | TF3; export function anyPass( - predicates: [(a: T) => a is TF1, (a: T) => a is TF2, (a: T) => a is TF3, (a: T) => a is TF4], + predicates: [ + (a: T) => a is TF1, + (a: T) => a is TF2, + (a: T) => a is TF3, + (a: T) => a is TF4 + ], ): (a: T) => a is TF1 | TF2 | TF3 | TF4; export function anyPass( predicates: [ @@ -29,4 +39,46 @@ export function anyPass a is TF6 ], ): (a: T) => a is TF1 | TF2 | TF3 | TF4 | TF5 | TF6; +// regular +export function anyPass( + predicates: [ + (a: T1) => boolean, + (a: T2) => boolean + ], +): (a: T1 & T2) => boolean; +export function anyPass( + predicates: [ + (a: T1) => boolean, + (a: T2) => boolean, + (a: T3) => boolean + ], +): (a: T1 & T2 & T3) => boolean; +export function anyPass( + predicates: [ + (a: T1) => boolean, + (a: T2) => boolean, + (a: T3) => boolean, + (a: T4) => boolean + ], +): (a: T1 & T2 & T3 & T4) => boolean; +export function anyPass( + predicates: [ + (a: T1) => boolean, + (a: T2) => boolean, + (a: T3) => boolean, + (a: T4) => boolean, + (a: T5) => boolean + ], +): (a: T1 & T2 & T3 & T4 & T5) => boolean; +export function anyPass( + predicates: [ + (a: T1) => boolean, + (a: T2) => boolean, + (a: T3) => boolean, + (a: T4) => boolean, + (a: T5) => boolean, + (a: T6) => boolean + ], +): (a: T1 & T2 & T3 & T4 & T5 & T6) => boolean; +// catch-all export function anyPass boolean>(predicates: readonly F[]): F; diff --git a/types/propEq.d.ts b/types/propEq.d.ts index e667fb2..d4f4a7d 100644 --- a/types/propEq.d.ts +++ b/types/propEq.d.ts @@ -1,6 +1,13 @@ +import { WidenLiterals } from './util/tools'; + +// propEq(val) export function propEq(val: T): { - (name: K): (obj: Record) => boolean; - (name: K, obj: Record): boolean; + // propEq(val)(name)(obj) + (name: K): >>(obj: Required extends Record ? T extends WidenLiterals ? U : never : never) => boolean; + // propEq(val)(name, obj) + >>(name: K, obj: Required extends Record ? T extends WidenLiterals ? U : never : never): boolean; }; -export function propEq(val: T, name: K): (obj: Record) => boolean; +// propEq(val, name)(obj) +export function propEq(val: T, name: K): >>(obj: Required extends Record ? T extends WidenLiterals ? U : never : never) => boolean; +// propEq(val, name, obj) export function propEq(val: U[K], name: K, obj: U): boolean;