From 0e662108353c591fc3ea29b23958fb0df1f122a2 Mon Sep 17 00:00:00 2001 From: harris-miller Date: Sun, 15 Oct 2023 12:26:51 -0600 Subject: [PATCH 1/9] mimic how comparitors work in typescript --- test/propEq.test.ts | 26 ++++++++++++++++++++------ types/propEq.d.ts | 42 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/test/propEq.test.ts b/test/propEq.test.ts index dc5b9098..b8a80b62 100644 --- a/test/propEq.test.ts +++ b/test/propEq.test.ts @@ -10,17 +10,31 @@ type Obj = { n: null; }; +// explanation +// `obj.union` is type `'A' | 'B'` and `val` is `string` +// typescript allows comparison because as long as one side extends the other, it's ok +function doesEq(val: string, obj: Obj) { + return obj.union === val; +} +// this is different from assignment that errors because`string` is too wide for `'A' | 'B'` +function assign(val: string, obj: Obj) { + // @ts-expect-error -- remove this to see the error (need this so `npm run test` pasts) + obj.union = val; +} + +// this is why we use `WidenLiterals` in the type definition + // propEq(val, name, obj) expectType(propEq('foo', 'union', {} as Obj)); -// non-union string fails -expectError(propEq('nope', 'union', {} as Obj)); +// any string works here, see the above explanation as to why +expectType(propEq('else', '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)); +// any string works here, see the above explanation as to why +expectType(propEq('else')('union')({} as Obj)); // completely different type fails expectError(propEq(2)('union')({} as Obj)); @@ -33,7 +47,7 @@ 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)); +// any string works here, see the above explanation as to why +expectType(propEq('else', 'union')({} as Obj)); // completely different type fails expectError(propEq(2, 'union')({} as Obj)); diff --git a/types/propEq.d.ts b/types/propEq.d.ts index e667fb28..805b1021 100644 --- a/types/propEq.d.ts +++ b/types/propEq.d.ts @@ -1,6 +1,40 @@ +import { Placeholder, 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: Record>) => boolean; + // propEq(val)(__, obj)(name) + (__: Placeholder, obj: U): (name: K) => U[K] extends WidenLiterals ? boolean : never; + // propEq(val)(name, obj) + (name: K, obj: Record>): boolean; }; -export function propEq(val: T, name: K): (obj: Record) => boolean; -export function propEq(val: U[K], name: K, obj: U): boolean; + +// propEq(__, name) +export function propEq(__: Placeholder, name: K): { + // propEq(val)(obj) + (val: T): (obj: Record>) => boolean; + // propEq(__, obj)(val) + >(__: Placeholder, obj: U): (val: WidenLiterals) => boolean; + // propEq(val, obj) + (val: T, obj: Record>): boolean; +}; +// propEq(val, name)(obj) +export function propEq(val: T, name: K): (obj: Record>) => boolean; + +// propEq(__, __, obj) +export function propEq(__: Placeholder, __2: Placeholder, obj: U): { + // propEq(__, __, obj)(val)(name) + (val: T): (name: K) => boolean; + // propEq(__, __, obj)(__, key)(name) + (__: Placeholder, key: K): (val: WidenLiterals) => boolean; + // propEq(__, __, obj)(name, key) + (val: WidenLiterals, name: K): boolean; +}; + +// propEq(__, name, obj)(val) +export function propEq(__: Placeholder, name: K, obj: U): (val: WidenLiterals) => boolean; +// propEq(val, __, obj)(name) +export function propEq(val: T, __: Placeholder, obj: U): (name: K) => U[K] extends WidenLiterals ? boolean : never; +// propEq(val, name, obj) +export function propEq(val: WidenLiterals, name: K, obj: U): boolean; From d4fe7fd873afd7e32e6269c09bc54de5bcc8b82d Mon Sep 17 00:00:00 2001 From: harris-miller Date: Sun, 15 Oct 2023 13:51:57 -0600 Subject: [PATCH 2/9] tests --- test/propEq.test.ts | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/test/propEq.test.ts b/test/propEq.test.ts index b8a80b62..1cf67110 100644 --- a/test/propEq.test.ts +++ b/test/propEq.test.ts @@ -13,10 +13,12 @@ type Obj = { // explanation // `obj.union` is type `'A' | 'B'` and `val` is `string` // typescript allows comparison because as long as one side extends the other, it's ok +// eslint-disable-next-line @typescript-eslint/no-unused-vars function doesEq(val: string, obj: Obj) { return obj.union === val; } // this is different from assignment that errors because`string` is too wide for `'A' | 'B'` +// eslint-disable-next-line @typescript-eslint/no-unused-vars function assign(val: string, obj: Obj) { // @ts-expect-error -- remove this to see the error (need this so `npm run test` pasts) obj.union = val; @@ -30,20 +32,44 @@ expectType(propEq('foo', 'union', {} as Obj)); expectType(propEq('else', 'union', {} as Obj)); // completely different type fails expectError(propEq(2, 'union', {} as Obj)); +// other props work as expected +expectType(propEq(2, 'num', {} as Obj)); +expectError(propEq(2, 'u', {} as Obj)); +expectType(propEq(2, 'num', {} as Obj)); +expectError(propEq(2, 'n', {} as Obj)); +expectType(propEq(null, 'n', {} as Obj)); +expectError(propEq(2, 'n', {} as Obj)); +expectType(propEq(undefined, 'u', {} as Obj)); // propEq(val)(name)(obj) expectType(propEq('foo')('union')({} as Obj)); // any string works here, see the above explanation as to why expectType(propEq('else')('union')({} as Obj)); // completely different type fails -expectError(propEq(2)('union')({} as Obj)); +expectError(propEq(2, 'union', {} as Obj)); +// other props work as expected +expectType(propEq(2, 'num', {} as Obj)); +expectError(propEq(2)('u')({} as Obj)); +expectType(propEq(2)('num')({} as Obj)); +expectError(propEq(2)('n')({} as Obj)); +expectType(propEq(null, 'n', {} as Obj)); +expectError(propEq(2)('n')({} as Obj)); +expectType(propEq(undefined, 'u', {} as Obj)); -// propEq(val)(name), 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)); +// other props work as expected +expectType(propEq(2)('num', {} as Obj)); +expectError(propEq(2)('u', {} as Obj)); +expectType(propEq(2)('num', {} as Obj)); +expectError(propEq(2)('n', {} as Obj)); +expectType(propEq(null)('n', {} as Obj)); +expectError(propEq(2)('n', {} as Obj)); +expectType(propEq(undefined, 'u', {} as Obj)); // propEq(val, name)(obj) expectType(propEq('foo', 'union')({} as Obj)); @@ -51,3 +77,11 @@ expectType(propEq('foo', 'union')({} as Obj)); expectType(propEq('else', 'union')({} as Obj)); // completely different type fails expectError(propEq(2, 'union')({} as Obj)); +// other props work as expected +expectType(propEq(2, 'num')({} as Obj)); +expectError(propEq(2, 'u')({} as Obj)); +expectType(propEq(2, 'num')({} as Obj)); +expectError(propEq(2, 'n')({} as Obj)); +expectType(propEq(null, 'n')({} as Obj)); +expectError(propEq(2, 'n')({} as Obj)); +expectType(propEq(undefined, 'u')({} as Obj)); From 55b4ce4a6e65557c8c19fcc9d993cd7135fb2fb3 Mon Sep 17 00:00:00 2001 From: harris-miller Date: Sun, 15 Oct 2023 14:11:56 -0600 Subject: [PATCH 3/9] need to figure out how to allow for null and undefined --- test/propEq.test.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/propEq.test.ts b/test/propEq.test.ts index 1cf67110..8ae8e89d 100644 --- a/test/propEq.test.ts +++ b/test/propEq.test.ts @@ -10,19 +10,16 @@ type Obj = { n: null; }; +const str: string = ''; // explanation -// `obj.union` is type `'A' | 'B'` and `val` is `string` -// typescript allows comparison because as long as one side extends the other, it's ok -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function doesEq(val: string, obj: Obj) { - return obj.union === val; -} -// this is different from assignment that errors because`string` is too wide for `'A' | 'B'` -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function assign(val: string, obj: Obj) { - // @ts-expect-error -- remove this to see the error (need this so `npm run test` pasts) - obj.union = val; -} + +// `string` is too wide for `'foo' | 'bar` for assignment +({} as Obj).union = str; +// but for comparison is fine +({} as Obj).union === str; +// null and undefined are allowed as well +({} as Obj).str === null; +({} as Obj).str === undefined; // this is why we use `WidenLiterals` in the type definition @@ -30,6 +27,9 @@ function assign(val: string, obj: Obj) { expectType(propEq('foo', 'union', {} as Obj)); // any string works here, see the above explanation as to why expectType(propEq('else', 'union', {} as Obj)); +// also null or undefined +expectType(propEq(null, 'union', {} as Obj)); +expectType(propEq(undefined, 'union', {} as Obj)); // completely different type fails expectError(propEq(2, 'union', {} as Obj)); // other props work as expected From 87420b6f574e5be50567f39075dc9fb25679539c Mon Sep 17 00:00:00 2001 From: harris-miller Date: Sun, 15 Oct 2023 17:11:55 -0600 Subject: [PATCH 4/9] back this up, lets do this a bit at a time --- types/propEq.d.ts | 38 ++++++-------------------------------- 1 file changed, 6 insertions(+), 32 deletions(-) diff --git a/types/propEq.d.ts b/types/propEq.d.ts index 805b1021..1be90fca 100644 --- a/types/propEq.d.ts +++ b/types/propEq.d.ts @@ -1,40 +1,14 @@ -import { Placeholder, WidenLiterals } from './util/tools'; +import { WidenLiterals } from './util/tools'; // propEq(val) export function propEq(val: T): { // propEq(val)(name)(obj) - (name: K): (obj: Record>) => boolean; - // propEq(val)(__, obj)(name) - (__: Placeholder, obj: U): (name: K) => U[K] extends WidenLiterals ? boolean : never; + (name: K): >>(obj: U) => T extends WidenLiterals ? boolean : never; // propEq(val)(name, obj) - (name: K, obj: Record>): boolean; -}; - -// propEq(__, name) -export function propEq(__: Placeholder, name: K): { - // propEq(val)(obj) - (val: T): (obj: Record>) => boolean; - // propEq(__, obj)(val) - >(__: Placeholder, obj: U): (val: WidenLiterals) => boolean; - // propEq(val, obj) - (val: T, obj: Record>): boolean; + // type it this way for better error message for unknown keys + >>(name: K, obj: U): T extends WidenLiterals ? boolean : never; }; // propEq(val, name)(obj) -export function propEq(val: T, name: K): (obj: Record>) => boolean; - -// propEq(__, __, obj) -export function propEq(__: Placeholder, __2: Placeholder, obj: U): { - // propEq(__, __, obj)(val)(name) - (val: T): (name: K) => boolean; - // propEq(__, __, obj)(__, key)(name) - (__: Placeholder, key: K): (val: WidenLiterals) => boolean; - // propEq(__, __, obj)(name, key) - (val: WidenLiterals, name: K): boolean; -}; - -// propEq(__, name, obj)(val) -export function propEq(__: Placeholder, name: K, obj: U): (val: WidenLiterals) => boolean; -// propEq(val, __, obj)(name) -export function propEq(val: T, __: Placeholder, obj: U): (name: K) => U[K] extends WidenLiterals ? boolean : never; +export function propEq(val: T, name: K): >>(obj: U) => T extends WidenLiterals ? boolean : never; // propEq(val, name, obj) -export function propEq(val: WidenLiterals, name: K, obj: U): boolean; +export function propEq(val: U[K], name: K, obj: U): boolean; From 021725d244e3c4e2e9b69909aee098b82933ada1 Mon Sep 17 00:00:00 2001 From: harris-miller Date: Sun, 15 Oct 2023 17:25:29 -0600 Subject: [PATCH 5/9] tests --- test/propEq.test.ts | 149 ++++++++++++++++++++++---------------------- 1 file changed, 75 insertions(+), 74 deletions(-) diff --git a/test/propEq.test.ts b/test/propEq.test.ts index 8ae8e89d..6a868062 100644 --- a/test/propEq.test.ts +++ b/test/propEq.test.ts @@ -3,85 +3,86 @@ 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'; + union: number | string; + nullable: number | null; + optional?: number; }; -const str: string = ''; -// explanation - -// `string` is too wide for `'foo' | 'bar` for assignment -({} as Obj).union = str; -// but for comparison is fine -({} as Obj).union === str; -// null and undefined are allowed as well -({} as Obj).str === null; -({} as Obj).str === undefined; - -// this is why we use `WidenLiterals` in the type definition - -// propEq(val, name, obj) -expectType(propEq('foo', 'union', {} as Obj)); -// any string works here, see the above explanation as to why -expectType(propEq('else', 'union', {} as Obj)); -// also null or undefined -expectType(propEq(null, 'union', {} as Obj)); -expectType(propEq(undefined, 'union', {} as Obj)); -// completely different type fails -expectError(propEq(2, 'union', {} as Obj)); -// other props work as expected -expectType(propEq(2, 'num', {} as Obj)); -expectError(propEq(2, 'u', {} as Obj)); -expectType(propEq(2, 'num', {} as Obj)); -expectError(propEq(2, 'n', {} as Obj)); -expectType(propEq(null, 'n', {} as Obj)); -expectError(propEq(2, 'n', {} as Obj)); -expectType(propEq(undefined, 'u', {} as Obj)); +const obj = {} as Obj; // propEq(val)(name)(obj) -expectType(propEq('foo')('union')({} as Obj)); -// any string works here, see the above explanation as to why -expectType(propEq('else')('union')({} as Obj)); -// completely different type fails -expectError(propEq(2, 'union', {} as Obj)); -// other props work as expected -expectType(propEq(2, 'num', {} as Obj)); -expectError(propEq(2)('u')({} as Obj)); -expectType(propEq(2)('num')({} as Obj)); -expectError(propEq(2)('n')({} as Obj)); -expectType(propEq(null, 'n', {} as Obj)); -expectError(propEq(2)('n')({} as Obj)); -expectType(propEq(undefined, 'u', {} as Obj)); +expectType(propEq('A')('literals')(obj)); +// any string works here and will return boolean, we can live with that +expectType(propEq('C')('literals')(obj)); +// a different base type returns `never` +expectType(propEq(2)('literals')(obj)); +// unions work +expectType(propEq(2)('union')(obj)); +expectType(propEq('2')('union')(obj)); +expectType(propEq(true)('union')(obj)); +// nullable works +expectType(propEq(2)('nullable')(obj)); +expectType(propEq(null)('nullable')(obj)); +// optionals work +expectType(propEq(2)('optional')(obj)); +expectType(propEq(undefined)('optional')(obj)); +// unknownKey errors +expectError(propEq('whatever')('unknownKey')(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)); -// other props work as expected -expectType(propEq(2)('num', {} as Obj)); -expectError(propEq(2)('u', {} as Obj)); -expectType(propEq(2)('num', {} as Obj)); -expectError(propEq(2)('n', {} as Obj)); -expectType(propEq(null)('n', {} as Obj)); -expectError(propEq(2)('n', {} as Obj)); -expectType(propEq(undefined, 'u', {} as Obj)); +expectType(propEq('A')('literals', obj)); +// any string works here and will return boolean, we can live with that +expectType(propEq('C')('literals', obj)); +// a different base type returns `never` +expectType(propEq(2)('literals', obj)); +// unions work +expectType(propEq(2)('union', obj)); +expectType(propEq('2')('union', obj)); +expectType(propEq(true)('union', obj)); +// nullable works +expectType(propEq(2)('nullable', obj)); +expectType(propEq(null)('nullable', obj)); +// optionals work +expectType(propEq(2)('optional', obj)); +expectType(propEq(undefined)('optional', obj)); +// unknownKey errors +expectError(propEq('whatever')('unknownKey', obj)); // propEq(val, name)(obj) -expectType(propEq('foo', 'union')({} as Obj)); -// any string works here, see the above explanation as to why -expectType(propEq('else', 'union')({} as Obj)); -// completely different type fails -expectError(propEq(2, 'union')({} as Obj)); -// other props work as expected -expectType(propEq(2, 'num')({} as Obj)); -expectError(propEq(2, 'u')({} as Obj)); -expectType(propEq(2, 'num')({} as Obj)); -expectError(propEq(2, 'n')({} as Obj)); -expectType(propEq(null, 'n')({} as Obj)); -expectError(propEq(2, 'n')({} as Obj)); -expectType(propEq(undefined, 'u')({} as Obj)); +expectType(propEq('A', 'literals')(obj)); +// any string works here and will return boolean, we can live with that +expectType(propEq('C', 'literals')(obj)); +// a different base type returns `never` +expectType(propEq(2, 'literals')(obj)); +// unions work +expectType(propEq(2, 'union')(obj)); +expectType(propEq('2', 'union')(obj)); +expectType(propEq(true, 'union')(obj)); +// nullable works +expectType(propEq(2, 'nullable')(obj)); +expectType(propEq(null, 'nullable')(obj)); +// optionals work +expectType(propEq(2, 'optional')(obj)); +expectType(propEq(undefined, 'optional')(obj)); +// unknownKey errors +expectError(propEq('whatever', 'unknownKey')(obj)); + +// propEq(val, name, obj) +expectType(propEq('A', 'literals', obj)); +// any string works here and will return boolean, we can live with that +expectError(propEq('C', 'literals', obj)); +// a different base type returns `never` +expectError(propEq(2, 'literals', obj)); +// unions work +expectType(propEq(2, 'union', obj)); +expectType(propEq('2', 'union', obj)); +expectError(propEq(true, 'union', obj)); +// nullable works +expectType(propEq(2, 'nullable', obj)); +expectType(propEq(null, 'nullable', obj)); +// optionals work +expectType(propEq(2, 'optional', obj)); +expectType(propEq(undefined, 'optional', obj)); +// unknownKey errors +expectError(propEq('whatever', 'unknownKey', obj)); From 806ea43bcc0b6caca9cd4f5efa30379b889feccd Mon Sep 17 00:00:00 2001 From: harris-miller Date: Wed, 3 Jan 2024 00:12:24 -0700 Subject: [PATCH 6/9] big big update --- test/propEq.test.ts | 217 ++++++++++++++++++++++++++++++-------------- types/propEq.d.ts | 10 +- 2 files changed, 151 insertions(+), 76 deletions(-) diff --git a/test/propEq.test.ts b/test/propEq.test.ts index 6a868062..434b95ff 100644 --- a/test/propEq.test.ts +++ b/test/propEq.test.ts @@ -4,85 +4,162 @@ import { propEq } from '../es'; type Obj = { literals: 'A' | 'B'; - union: number | string; - nullable: number | null; + unions: number | string; + nullable: number | null | undefined; optional?: number; }; const obj = {} as Obj; -// propEq(val)(name)(obj) +const literalVar = 'A'; +let typedVar: 'A' | 'B' = 'A'; + +// +// literals +// + +// happy path works as expected expectType(propEq('A')('literals')(obj)); -// any string works here and will return boolean, we can live with that -expectType(propEq('C')('literals')(obj)); -// a different base type returns `never` -expectType(propEq(2)('literals')(obj)); -// unions work -expectType(propEq(2)('union')(obj)); -expectType(propEq('2')('union')(obj)); -expectType(propEq(true)('union')(obj)); -// nullable works -expectType(propEq(2)('nullable')(obj)); -expectType(propEq(null)('nullable')(obj)); -// optionals work -expectType(propEq(2)('optional')(obj)); -expectType(propEq(undefined)('optional')(obj)); -// unknownKey errors -expectError(propEq('whatever')('unknownKey')(obj)); - -// propEq(val)(name, obj) -expectType(propEq('A')('literals', obj)); -// any string works here and will return boolean, we can live with that -expectType(propEq('C')('literals', obj)); -// a different base type returns `never` -expectType(propEq(2)('literals', obj)); -// unions work -expectType(propEq(2)('union', obj)); -expectType(propEq('2')('union', obj)); -expectType(propEq(true)('union', obj)); -// nullable works -expectType(propEq(2)('nullable', obj)); -expectType(propEq(null)('nullable', obj)); -// optionals work -expectType(propEq(2)('optional', obj)); -expectType(propEq(undefined)('optional', obj)); -// unknownKey errors -expectError(propEq('whatever')('unknownKey', obj)); - -// propEq(val, name)(obj) expectType(propEq('A', 'literals')(obj)); -// any string works here and will return boolean, we can live with that -expectType(propEq('C', 'literals')(obj)); -// a different base type returns `never` -expectType(propEq(2, 'literals')(obj)); -// unions work -expectType(propEq(2, 'union')(obj)); -expectType(propEq('2', 'union')(obj)); -expectType(propEq(true, 'union')(obj)); -// nullable works -expectType(propEq(2, 'nullable')(obj)); -expectType(propEq(null, 'nullable')(obj)); -// optionals work -expectType(propEq(2, 'optional')(obj)); -expectType(propEq(undefined, 'optional')(obj)); -// unknownKey errors -expectError(propEq('whatever', 'unknownKey')(obj)); - -// propEq(val, name, obj) expectType(propEq('A', 'literals', obj)); -// any string works here and will return boolean, we can live with that + +// rejects if typeof val not U[K] +expectError(propEq('C')('literals')(obj)); +expectError(propEq('C', 'literals')(obj)); expectError(propEq('C', 'literals', obj)); -// a different base type returns `never` + +expectError(propEq(2)('literals')(obj)); +expectError(propEq(2, 'literals')(obj)); expectError(propEq(2, 'literals', obj)); -// unions work -expectType(propEq(2, 'union', obj)); -expectType(propEq('2', 'union', obj)); -expectError(propEq(true, 'union', obj)); -// nullable works -expectType(propEq(2, 'nullable', obj)); + +// works for variable literal of correct type +expectType(propEq(literalVar)('literals')(obj)); +expectType(propEq(literalVar, 'literals')(obj)); +expectType(propEq(literalVar, 'literals', obj)); + +// works for variable typed to be same +expectType(propEq(typedVar)('literals')(obj)); +expectType(propEq(typedVar, 'literals')(obj)); +expectType(propEq(typedVar, 'literals', obj)); + +// rejects if typeof val is too wide +expectError(propEq('A' as string)('literals')(obj)); +expectError(propEq('A' as string, 'literals')(obj)); +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)); -// optionals work -expectType(propEq(2, 'optional', 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)); -// unknownKey errors -expectError(propEq('whatever', 'unknownKey', 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/propEq.d.ts b/types/propEq.d.ts index 1be90fca..c2a2d029 100644 --- a/types/propEq.d.ts +++ b/types/propEq.d.ts @@ -1,14 +1,12 @@ -import { WidenLiterals } from './util/tools'; - // propEq(val) -export function propEq(val: T): { +export function propEq(val: T): { // propEq(val)(name)(obj) - (name: K): >>(obj: U) => T extends WidenLiterals ? boolean : never; + (name: K): >>(obj: Required extends Record ? T extends U[K] ? U : never : never) => boolean; // propEq(val)(name, obj) // type it this way for better error message for unknown keys - >>(name: K, obj: U): T extends WidenLiterals ? boolean : never; + >>(name: K, obj: Required extends Record ? T extends U[K] ? U : never : never): boolean; }; // propEq(val, name)(obj) -export function propEq(val: T, name: K): >>(obj: U) => T extends WidenLiterals ? boolean : never; +export function propEq(val: T, name: K): >>(obj: Required extends Record ? T extends U[K] ? U : never : never) => boolean; // propEq(val, name, obj) export function propEq(val: U[K], name: K, obj: U): boolean; From 82e6d37202bfa0ddb04e835372c844d6b3548e58 Mon Sep 17 00:00:00 2001 From: harris-miller Date: Wed, 3 Jan 2024 01:35:32 -0700 Subject: [PATCH 7/9] that completely broke passing to other functions expecting non-never args or return types --- test/allPass.test.ts | 18 ++++++++++++++++++ test/anyPass.test.ts | 29 ++++++++++++++++++++++++++--- types/propEq.d.ts | 10 ++++++---- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/test/allPass.test.ts b/test/allPass.test.ts index 37040dd0..1e3ee1cf 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 ff46aa58..6928ddca 100644 --- a/test/anyPass.test.ts +++ b/test/anyPass.test.ts @@ -25,13 +25,33 @@ expectType( }) ); +expectType( + isVampire({ + age: 300, // any number + garlic_allergy: false, // any bool + sun_allergy: false, // any bool + fast: null + // fear: undefined // can leave out because `undefined` are considered optional + }) +); + +expectError( + isVampire({ + age: 21, + garlic_allergy: true, + sun_allergy: true, + fast: false, // wrong type + fear: undefined + }) +); + expectError( isVampire({ age: 21, garlic_allergy: true, sun_allergy: true, - fast: false, - fear: true + fast: null, + fear: true // wrong type }) ); @@ -39,7 +59,9 @@ expectError( isVampire({ age: 40, garlic_allergy: true, - fear: false + // sun_allergy: true, // can't have missing prop + fast: null, + fear: undefined }) ); @@ -48,3 +70,4 @@ expectError( nickname: 'Blade' }) ); + diff --git a/types/propEq.d.ts b/types/propEq.d.ts index c2a2d029..fd8a765d 100644 --- a/types/propEq.d.ts +++ b/types/propEq.d.ts @@ -1,12 +1,14 @@ +import { WidenLiterals } from './util/tools'; + // propEq(val) -export function propEq(val: T): { +export function propEq(val: T): { // propEq(val)(name)(obj) - (name: K): >>(obj: Required extends Record ? T extends U[K] ? U : never : never) => boolean; + (name: K): (obj: Record>) => boolean; // propEq(val)(name, obj) // type it this way for better error message for unknown keys - >>(name: K, obj: Required extends Record ? T extends U[K] ? U : never : never): boolean; + >>(name: K, obj: U): boolean; }; // propEq(val, name)(obj) -export function propEq(val: T, name: K): >>(obj: Required extends Record ? T extends U[K] ? U : never : never) => boolean; +export function propEq(val: T, name: K): (obj: Record>) => boolean; // propEq(val, name, obj) export function propEq(val: U[K], name: K, obj: U): boolean; From a04da4d4c52b8a207261ce68f43a6d6a04229964 Mon Sep 17 00:00:00 2001 From: harris-miller Date: Wed, 3 Jan 2024 02:20:39 -0700 Subject: [PATCH 8/9] needed to update anyPass and allPass to make these work --- test/anyPass.test.ts | 48 ++++++++++++++++----------------- test/propEq.test.ts | 25 +++++++---------- types/allPass.d.ts | 61 ++++++++++++++++++++++++++++++++++++++--- types/anyPass.d.ts | 64 +++++++++++++++++++++++++++++++++++++++----- types/propEq.d.ts | 7 +++-- 5 files changed, 152 insertions(+), 53 deletions(-) diff --git a/test/anyPass.test.ts b/test/anyPass.test.ts index 6928ddca..e9923872 100644 --- a/test/anyPass.test.ts +++ b/test/anyPass.test.ts @@ -26,32 +26,12 @@ expectType( ); expectType( - isVampire({ - age: 300, // any number - garlic_allergy: false, // any bool - sun_allergy: false, // any bool - fast: null - // fear: undefined // can leave out because `undefined` are considered optional - }) -); - -expectError( - isVampire({ - age: 21, - garlic_allergy: true, - sun_allergy: true, - fast: false, // wrong type - fear: undefined - }) -); - -expectError( isVampire({ age: 21, garlic_allergy: true, sun_allergy: true, fast: null, - fear: true // wrong type + fear: undefined }) ); @@ -59,9 +39,7 @@ expectError( isVampire({ age: 40, garlic_allergy: true, - // sun_allergy: true, // can't have missing prop - fast: null, - fear: undefined + fear: false }) ); @@ -71,3 +49,25 @@ expectError( }) ); +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 434b95ff..9b350cc6 100644 --- a/test/propEq.test.ts +++ b/test/propEq.test.ts @@ -23,28 +23,21 @@ expectType(propEq('A')('literals')(obj)); expectType(propEq('A', 'literals')(obj)); expectType(propEq('A', 'literals', obj)); -// rejects if typeof val not U[K] -expectError(propEq('C')('literals')(obj)); -expectError(propEq('C', '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)); -// works for variable literal of correct type -expectType(propEq(literalVar)('literals')(obj)); -expectType(propEq(literalVar, 'literals')(obj)); -expectType(propEq(literalVar, 'literals', obj)); - -// works for variable typed to be same -expectType(propEq(typedVar)('literals')(obj)); -expectType(propEq(typedVar, 'literals')(obj)); -expectType(propEq(typedVar, 'literals', obj)); - -// rejects if typeof val is too wide -expectError(propEq('A' as string)('literals')(obj)); -expectError(propEq('A' as string, '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 diff --git a/types/allPass.d.ts b/types/allPass.d.ts index d4027384..f3a0cc6a 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 a056d84c..48ca9664 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 fd8a765d..216c0d40 100644 --- a/types/propEq.d.ts +++ b/types/propEq.d.ts @@ -3,12 +3,11 @@ import { WidenLiterals } from './util/tools'; // propEq(val) export function propEq(val: T): { // propEq(val)(name)(obj) - (name: K): (obj: Record>) => boolean; + (name: K): >(obj: T extends WidenLiterals ? U : never) => boolean; // propEq(val)(name, obj) - // type it this way for better error message for unknown keys - >>(name: K, obj: U): boolean; + >(name: K, obj: T extends WidenLiterals ? U : never): boolean; }; // propEq(val, name)(obj) -export function propEq(val: T, name: K): (obj: Record>) => boolean; +export function propEq(val: T, name: K): >(obj: T extends WidenLiterals ? U : never) => boolean; // propEq(val, name, obj) export function propEq(val: U[K], name: K, obj: U): boolean; From aab5114d9e7e43346886a7efdf5107c2d9ce41c1 Mon Sep 17 00:00:00 2001 From: harris-miller Date: Wed, 3 Jan 2024 03:44:57 -0700 Subject: [PATCH 9/9] fix the optional problem --- test/propEq.test.ts | 3 --- types/propEq.d.ts | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/test/propEq.test.ts b/test/propEq.test.ts index 9b350cc6..89d70b6a 100644 --- a/test/propEq.test.ts +++ b/test/propEq.test.ts @@ -11,9 +11,6 @@ type Obj = { const obj = {} as Obj; -const literalVar = 'A'; -let typedVar: 'A' | 'B' = 'A'; - // // literals // diff --git a/types/propEq.d.ts b/types/propEq.d.ts index 216c0d40..d4f4a7de 100644 --- a/types/propEq.d.ts +++ b/types/propEq.d.ts @@ -3,11 +3,11 @@ import { WidenLiterals } from './util/tools'; // propEq(val) export function propEq(val: T): { // propEq(val)(name)(obj) - (name: K): >(obj: T extends WidenLiterals ? U : never) => boolean; + (name: K): >>(obj: Required extends Record ? T extends WidenLiterals ? U : never : never) => boolean; // propEq(val)(name, obj) - >(name: K, obj: T extends WidenLiterals ? U : never): boolean; + >>(name: K, obj: Required extends Record ? T extends WidenLiterals ? U : never : never): boolean; }; // propEq(val, name)(obj) -export function propEq(val: T, name: K): >(obj: T extends WidenLiterals ? U : never) => boolean; +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;