From bcf18241f14e40a7c763d7c21a2f8e3cb93f7e76 Mon Sep 17 00:00:00 2001 From: ersimont <8042088+ersimont@users.noreply.github.com> Date: Thu, 26 Nov 2020 19:20:46 -0500 Subject: [PATCH] feat(micro-dash): improve the typing of `pickBy()` --- TODO.md | 2 +- projects/micro-dash/src/lib/interfaces.ts | 19 ++- projects/micro-dash/src/lib/object/pick-by.ts | 67 ++++++--- .../micro-dash/src/test-helpers/test-utils.ts | 4 + .../typing-tests/object/pick-by.dts-spec.ts | 130 +++++++++++------- 5 files changed, 147 insertions(+), 75 deletions(-) diff --git a/TODO.md b/TODO.md index e952a61c..f544eb0b 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,4 @@ -- update readmes in old repos - landing page to link to all API docs - coveralls - help may be here, to combine multiple coverage runs into one report: https://github.com/angular/angular-cli/issues/11268 +- see if typedoc can support that "lib" mode that would eliminate the need for @hidden yet diff --git a/projects/micro-dash/src/lib/interfaces.ts b/projects/micro-dash/src/lib/interfaces.ts index 8a21dbc2..c7b2cbb4 100644 --- a/projects/micro-dash/src/lib/interfaces.ts +++ b/projects/micro-dash/src/lib/interfaces.ts @@ -38,18 +38,27 @@ export type Cast = Exclude extends never ? I : O; /** @hidden */ export type Narrow = Extract | Extract; /** @hidden */ -export type IfCouldBe = Extract extends never - ? Extract extends never - ? Else - : If +export type IfCouldBe = Narrow extends never + ? Else : If; /** @hidden */ -export type IfIndexType = string extends T +export type IfIndexType = string extends T ? If : number extends T ? If : Else; +/** @hidden */ +type IndexKeys = { [K in keyof T]: IfIndexType }[keyof T]; +/** @hidden */ +type NonIndexKeys = { [K in keyof T]: IfIndexType }[keyof T]; +/** @hidden */ +export type PartialExceptIndexes = { [K in IndexKeys]: T[K] } & + { [K in NonIndexKeys]?: T[K] }; + +/** @hidden */ +export type Evaluate = T extends infer I ? { [K in keyof I]: T[K] } : never; + /** @hidden */ export type Drop1Arg = T extends ( arg1: any, diff --git a/projects/micro-dash/src/lib/object/pick-by.ts b/projects/micro-dash/src/lib/object/pick-by.ts index 45ce4bc9..bf64be03 100644 --- a/projects/micro-dash/src/lib/object/pick-by.ts +++ b/projects/micro-dash/src/lib/object/pick-by.ts @@ -1,13 +1,49 @@ import { Cast, + Evaluate, IfCouldBe, + IfIndexType, KeyNarrowingIteratee, + Narrow, Nil, ObjectIteratee, + PartialExceptIndexes, ValueNarrowingIteratee, } from '../interfaces'; import { forOwn } from './for-own'; +/** @hidden */ +type IfDefinitelyIncluded = Exclude extends never + ? If + : Else; +/** @hidden */ +type IfMaybeIncluded = IfDefinitelyIncluded< + T, + O, + Else, + IfCouldBe +>; +/** @hidden */ +type KeysWithDefinitelyIncludedValues = { + [K in keyof T]: IfDefinitelyIncluded; +}[keyof T]; +/** @hidden */ +type KeysWithMaybeIncludedValues = { + [K in keyof T]: IfMaybeIncluded; +}[keyof T]; +/** @hidden */ +export type DefinitelyIncludedKeys = { + [K in keyof T]: IfIndexType< + K, + IfCouldBe, K>>, + IfDefinitelyIncluded, O, K> + >; +}[keyof T]; +/** @hidden */ +type MaybeIncludedKeys = { + [K in keyof T]: IfIndexType, O, K>>; +}[keyof T]; + /** * Creates an object composed of the `object` properties `predicate` returns truthy for. * @@ -22,7 +58,7 @@ import { forOwn } from './for-own'; export function pickBy( object: T[] | Nil, predicate: ValueNarrowingIteratee, -): { [index: number]: Extract | Extract }; +): { [index: number]: Narrow }; export function pickBy( object: T[] | Nil, predicate: ObjectIteratee, @@ -31,28 +67,25 @@ export function pickBy( export function pickBy, O>( object: I, predicate: ValueNarrowingIteratee, -): - | { - [K in { [KK in keyof T]: IfCouldBe }[keyof T]]: - | Extract - | Extract - | (Exclude extends never ? never : undefined); - } - | IfCouldBe; +): Evaluate< + | ({ [K in KeysWithDefinitelyIncludedValues]: Narrow } & + { [K in KeysWithMaybeIncludedValues]?: Narrow }) + | IfCouldBe +>; export function pickBy, O>( object: I, predicate: KeyNarrowingIteratee, -): - | { - [K in { [KK in keyof T]: IfCouldBe, O, KK> }[keyof T]]: - | T[K] - | (Cast extends O ? never : undefined); - } - | IfCouldBe; +): Evaluate< + | ({ + [K in DefinitelyIncludedKeys]: T[K]; + } & + { [K in MaybeIncludedKeys]?: T[K] }) + | IfCouldBe +>; export function pickBy( object: T, predicate: ObjectIteratee, -): Partial>; +): Evaluate>>; export function pickBy( object: T, diff --git a/projects/micro-dash/src/test-helpers/test-utils.ts b/projects/micro-dash/src/test-helpers/test-utils.ts index 5248b218..46602111 100644 --- a/projects/micro-dash/src/test-helpers/test-utils.ts +++ b/projects/micro-dash/src/test-helpers/test-utils.ts @@ -51,3 +51,7 @@ export function keyIsString(_: any, key: any): key is string { export function keyIsNumber(_: any, key: any): key is number { return isNumber(key); } + +export function keyIsDateOrString(_: any, key: any): key is string | Date { + return isString(key) || isDate(key); +} diff --git a/projects/micro-dash/src/typing-tests/object/pick-by.dts-spec.ts b/projects/micro-dash/src/typing-tests/object/pick-by.dts-spec.ts index 084f8893..47203d3e 100644 --- a/projects/micro-dash/src/typing-tests/object/pick-by.dts-spec.ts +++ b/projects/micro-dash/src/typing-tests/object/pick-by.dts-spec.ts @@ -10,6 +10,7 @@ import { keyIsAorC, keyIsAorNumber, keyIsC, + keyIsDateOrString, keyIsNumber, keyIsString, keyIsString2, @@ -86,14 +87,14 @@ interface O { 2: string; c: Date | Document; } -const o = { a: 1, 2: 'b', c: document } as O; -const oOrU = o as O | undefined; -const oOrN = o as O | null; -// $ExpectType Partial +declare const o: O; +declare const oOrU: O | undefined; +declare const oOrN: O | null; +// $ExpectType { 2?: string | undefined; a?: number | undefined; c?: Date | Document | undefined; } pickBy(o, () => true); -// $ExpectType Partial +// $ExpectType { 2?: string | undefined; a?: number | undefined; c?: Date | Document | undefined; } pickBy(oOrU, () => true); -// $ExpectType Partial +// $ExpectType { 2?: string | undefined; a?: number | undefined; c?: Date | Document | undefined; } pickBy(oOrN, () => true); // value narrowing @@ -105,11 +106,11 @@ pickBy(oOrU, isString); // $ExpectType {} | { 2: string; } pickBy(oOrN, isString); -// $ExpectType { c: Date | undefined; } +// $ExpectType { c?: Date | undefined; } pickBy(o, isDate); -// $ExpectType {} | { c: Date | undefined; } +// $ExpectType {} | { c?: Date | undefined; } pickBy(oOrU, isDate); -// $ExpectType {} | { c: Date | undefined; } +// $ExpectType {} | { c?: Date | undefined; } pickBy(oOrN, isDate); // $ExpectType { 2: string; a: number; } @@ -119,11 +120,11 @@ pickBy(oOrU, isNumberOrString); // $ExpectType {} | { 2: string; a: number; } pickBy(oOrN, isNumberOrString); -// $ExpectType { 2: string; c: Date | undefined; } +// $ExpectType { 2: string; c?: Date | undefined; } pickBy(o, isDateOrString); -// $ExpectType {} | { 2: string; c: Date | undefined; } +// $ExpectType {} | { 2: string; c?: Date | undefined; } pickBy(oOrU, isDateOrString); -// $ExpectType {} | { 2: string; c: Date | undefined; } +// $ExpectType {} | { 2: string; c?: Date | undefined; } pickBy(oOrN, isDateOrString); // $ExpectType {} @@ -143,20 +144,20 @@ pickBy(oOrN, isMapOrString); interface S2 { a: 'a' | number; } -const s2 = { a: 2 } as S2; -const s2OrU = { a: 2 } as S2 | undefined; -const s2OrN = { a: 2 } as S2 | null; -// $ExpectType { a: "a" | undefined; } +declare const s2: S2; +declare const s2OrU: S2 | undefined; +declare const s2OrN: S2 | null; +// $ExpectType { a?: "a" | undefined; } pickBy(s2, isA); -// $ExpectType {} | { a: "a" | undefined; } +// $ExpectType {} | { a?: "a" | undefined; } pickBy(s2OrU, isA); -// $ExpectType {} | { a: "a" | undefined; } +// $ExpectType {} | { a?: "a" | undefined; } pickBy(s2OrN, isA); -// $ExpectType { a: 2 | "a" | undefined; } +// $ExpectType { a?: 2 | "a" | undefined; } pickBy(s2, isStringOr2); -// $ExpectType {} | { a: 2 | "a" | undefined; } +// $ExpectType {} | { a?: 2 | "a" | undefined; } pickBy(s2OrU, isStringOr2); -// $ExpectType {} | { a: 2 | "a" | undefined; } +// $ExpectType {} | { a?: 2 | "a" | undefined; } pickBy(s2OrN, isStringOr2); // key narrowing @@ -202,11 +203,11 @@ pickBy(s, keyIsA); pickBy(sOrU, keyIsA); // $ExpectType {} | { a: number; } pickBy(sOrN, keyIsA); -// $ExpectType { 2: string | undefined; a: number; } +// $ExpectType { a: number; 2?: string | undefined; } pickBy(o, keyIsA); -// $ExpectType {} | { 2: string | undefined; a: number; } +// $ExpectType {} | { a: number; 2?: string | undefined; } pickBy(oOrU, keyIsA); -// $ExpectType {} | { 2: string | undefined; a: number; } +// $ExpectType {} | { a: number; 2?: string | undefined; } pickBy(oOrN, keyIsA); // $ExpectType {} @@ -215,11 +216,11 @@ pickBy(s, keyIsString2); pickBy(sOrU, keyIsString2); // $ExpectType {} | {} pickBy(sOrN, keyIsString2); -// $ExpectType { 2: string | undefined; } +// $ExpectType { 2?: string | undefined; } pickBy(o, keyIsString2); -// $ExpectType {} | { 2: string | undefined; } +// $ExpectType {} | { 2?: string | undefined; } pickBy(oOrU, keyIsString2); -// $ExpectType {} | { 2: string | undefined; } +// $ExpectType {} | { 2?: string | undefined; } pickBy(oOrN, keyIsString2); // $ExpectType {} @@ -228,11 +229,11 @@ pickBy(s, keyIsString3); pickBy(sOrU, keyIsString3); // $ExpectType {} | {} pickBy(sOrN, keyIsString3); -// $ExpectType { 2: string | undefined; } +// $ExpectType { 2?: string | undefined; } pickBy(o, keyIsString3); -// $ExpectType {} | { 2: string | undefined; } +// $ExpectType {} | { 2?: string | undefined; } pickBy(oOrU, keyIsString3); -// $ExpectType {} | { 2: string | undefined; } +// $ExpectType {} | { 2?: string | undefined; } pickBy(oOrN, keyIsString3); // $ExpectType { c: Date | Document; } @@ -241,11 +242,11 @@ pickBy(s, keyIsC); pickBy(sOrU, keyIsC); // $ExpectType {} | { c: Date | Document; } pickBy(sOrN, keyIsC); -// $ExpectType { 2: string | undefined; c: Date | Document; } +// $ExpectType { c: Date | Document; 2?: string | undefined; } pickBy(o, keyIsC); -// $ExpectType {} | { 2: string | undefined; c: Date | Document; } +// $ExpectType {} | { c: Date | Document; 2?: string | undefined; } pickBy(oOrU, keyIsC); -// $ExpectType {} | { 2: string | undefined; c: Date | Document; } +// $ExpectType {} | { c: Date | Document; 2?: string | undefined; } pickBy(oOrN, keyIsC); // $ExpectType { a: number; c: Date | Document; } @@ -254,11 +255,11 @@ pickBy(s, keyIsAorC); pickBy(sOrU, keyIsAorC); // $ExpectType {} | { a: number; c: Date | Document; } pickBy(sOrN, keyIsAorC); -// $ExpectType { 2: string | undefined; a: number; c: Date | Document; } +// $ExpectType { a: number; c: Date | Document; 2?: string | undefined; } pickBy(o, keyIsAorC); -// $ExpectType {} | { 2: string | undefined; a: number; c: Date | Document; } +// $ExpectType {} | { a: number; c: Date | Document; 2?: string | undefined; } pickBy(oOrU, keyIsAorC); -// $ExpectType {} | { 2: string | undefined; a: number; c: Date | Document; } +// $ExpectType {} | { a: number; c: Date | Document; 2?: string | undefined; } pickBy(oOrN, keyIsAorC); // $ExpectType { a: number; } @@ -267,25 +268,50 @@ pickBy(s, keyIsAorNumber); pickBy(sOrU, keyIsAorNumber); // $ExpectType {} | { a: number; } pickBy(sOrN, keyIsAorNumber); -// $ExpectType { 2: string | undefined; a: number; } +// $ExpectType { a: number; 2?: string | undefined; } pickBy(o, keyIsAorNumber); -// $ExpectType {} | { 2: string | undefined; a: number; } +// $ExpectType {} | { a: number; 2?: string | undefined; } pickBy(oOrU, keyIsAorNumber); -// $ExpectType {} | { 2: string | undefined; a: number; } +// $ExpectType {} | { a: number; 2?: string | undefined; } pickBy(oOrN, keyIsAorNumber); -const so = {} as { [key: string]: number | string }; -// $ExpectType { [x: string]: string | undefined; } -pickBy(so, isString); -// $ExpectType { [x: string]: number | undefined; } -pickBy(so, isNumber); +declare const record: Record; // $ExpectType {} -pickBy(so, isDate); -// $ExpectType { [x: string]: string | undefined; } -pickBy(so, isDateOrString); -// $ExpectType { [x: string]: string | number; } -pickBy(so, keyIsString); -// $ExpectType { [x: string]: string | number | undefined; } -pickBy(so, keyIsA); +pickBy(record, isString); +// $ExpectType { [x: string]: number; } +pickBy(record, isNumber); +// $ExpectType { [x: string]: number; } +pickBy(record, keyIsString); // $ExpectType {} -pickBy(so, keyIsNumber); +pickBy(record, keyIsNumber); +// $ExpectType { [x: string]: number; } +pickBy(record, keyIsDateOrString); +// $ExpectType { a: number; } +pickBy(record, keyIsA); +// $ExpectType { [x: string]: number; } +pickBy(record, () => true); + +declare const composite: { + [k: number]: string | Date; + a: 'eh'; +}; +// $ExpectType { [x: number]: string | undefined; a: "eh"; } +pickBy(composite, isString); +// $ExpectType {} +pickBy(composite, isNumber); +// $ExpectType { [x: number]: Date | undefined; } +pickBy(composite, isDate); +// $ExpectType { [x: number]: string | Date; a: "eh"; } +pickBy(composite, isDateOrString); +// $ExpectType { [x: number]: string | Date; a: "eh"; } +pickBy(composite, keyIsString); +// $ExpectType { [x: number]: string | Date; a: "eh"; } +pickBy(composite, keyIsA); +// $ExpectType {} +pickBy(composite, keyIsNumber); +// $ExpectType { [x: number]: string | Date; a: "eh"; } +pickBy(composite, keyIsAorC); +// $ExpectType { [x: number]: string | Date; a: "eh"; } +pickBy(composite, keyIsAorNumber); +// $ExpectType { [x: number]: string | Date; a?: "eh" | undefined; } +pickBy(composite, () => true);