diff --git a/src/functional.ts b/src/functional.ts index 97de8d3d117..8a4946b7273 100644 --- a/src/functional.ts +++ b/src/functional.ts @@ -1,3 +1,5 @@ +import { LeafElems } from "./metaprogramming"; + /** * Flattens an object so that the return value's keys are the path * to a value in the source object. E.g. flattenObject({the: {answer: 42}}) @@ -5,14 +7,14 @@ * @param obj An object to be flattened * @return An array where values come from obj and keys are the path in obj to that value. */ -export function* flattenObject(obj: Record): Generator<[string, unknown]> { - function* helper(path: string[], obj: Record): Generator<[string, unknown]> { +export function* flattenObject(obj: T): Generator<[string, unknown]> { + function* helper(path: string[], obj: V): Generator<[string, unknown]> { for (const [k, v] of Object.entries(obj)) { if (typeof v !== "object" || v === null) { yield [[...path, k].join("."), v]; } else { // Object.entries loses type info, so we must cast - yield* helper([...path, k], v as Record); + yield* helper([...path, k], v); } } } @@ -25,33 +27,35 @@ export function* flattenObject(obj: Record): Generator<[string, * [...flatten([[[1]], [2], 3])] = [1, 2, 3] */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function* flattenArray(arr: unknown[]): Generator { +export function* flattenArray(arr: T): Generator> { for (const val of arr) { if (Array.isArray(val)) { yield* flattenArray(val); } else { - yield val as T; + yield val as LeafElems; } } } /** Shorthand for flattenObject. */ -export function flatten(obj: Record): Generator<[string, unknown]>; +export function flatten(obj: T): Generator<[string, string]>; /** Shorthand for flattenArray. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function flatten(arr: unknown[]): Generator; +export function flatten(arr: T): Generator>; /** Flattens an object or array. */ -export function flatten( - objOrArr: Record | unknown[] -): Generator<[string, unknown]> | Generator { +export function flatten(objOrArr: T): unknown { if (Array.isArray(objOrArr)) { - return flattenArray(objOrArr); + return flattenArray(objOrArr); } else { return flattenObject(objOrArr); } } +type RecursiveElems = { + [Key in keyof T]: T[Key] extends unknown[] ? T[Key] | RecursiveElems : T[Key]; +}[number]; + /** * Used with reduce to flatten in place. * Due to the quirks of TypeScript, callers must pass [] as the @@ -59,7 +63,7 @@ export function flatten( */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function reduceFlat(accum: T[] | undefined, next: unknown): T[] { - return [...(accum || []), ...flatten([next])]; + return [...(accum || []), ...(flatten([next]) as Generator)]; } /** diff --git a/src/metaprogramming.ts b/src/metaprogramming.ts new file mode 100644 index 00000000000..95ebc81f9bd --- /dev/null +++ b/src/metaprogramming.ts @@ -0,0 +1,83 @@ +type Primitive = string | number | boolean | Function; + +/** + * RecursiveKeyOf is a type for keys of an objet usind dots for subfields. + * For a given object: {a: {b: {c: number}}, d } the RecursiveKeysOf are + * 'a' | 'a.b' | 'a.b.c' | 'd' + */ +export type RecursiveKeyOf = T extends Primitive + ? never + : + | (keyof T & string) + | { + [P in keyof T & string]: RecursiveSubKeys; + }[keyof T & string]; + +type RecursiveSubKeys = T[P] extends (infer Elem)[] + ? `${P}.${RecursiveKeyOf}` + : T[P] extends object + ? `${P}.${RecursiveKeyOf}` + : never; + +/** + * LeafKeysOf is like RecursiveKeysOf but omits the keys for any object. + * For a given object: {a: {b: {c: number}}, d } the LeafKeysOf are + * 'a.b.c' | 'd' + */ +export type LeafKeysOf = { + [Key in keyof T & (string | number)]: T[Key] extends unknown[] + ? `${Key}` + : T[Key] extends object + ? `${Key}.${RecursiveKeyOf}` + : `${Key}`; +}[keyof T & (string | number)]; + +/** + * SameType is used in testing to verify that two types are the same. + * Usage: + * const test: SameType = true. + * The assigment will fail if the types are different. + */ +export type SameType = T extends V ? (V extends T ? true : false) : false; + +type HeadOf = [T extends `${infer Head}.${infer Tail}` ? Head : T][number]; + +type TailsOf = [ + T extends `${Head}.${infer Tail}` ? Tail : never +][number]; + +/** + * DeepOmit allows you to omit fields from a nested structure using recursive keys. + */ +export type DeepOmit> = DeepOmitUnsafe; + +type DeepOmitUnsafe = { + [Key in Exclude]: Key extends Keys + ? T[Key] | undefined + : Key extends HeadOf + ? DeepOmitUnsafe> + : T[Key]; +}; + +export type DeepPick> = DeepPickUnsafe; + +type DeepPickUnsafe = { + [Key in Extract>]: Key extends Keys + ? T[Key] + : DeepPickUnsafe>; +}; + +/** In the array LeafElems<[[["a"], "b"], ["c"]]> is "a" | "b" | "c" */ +export type LeafElems = T extends Array + ? Elem extends unknown[] + ? LeafElems + : Elem + : T; + +/** + * In the object {a: number, b: { c: string } }, + * LeafValues is number | string + */ +export type LeafValues = { + [Key in keyof T]: T[Key] extends object ? LeafValues : T[Key]; +}[keyof T]; diff --git a/src/test/functional.spec.ts b/src/test/functional.spec.ts index 357835b365f..753b4ae5aa3 100644 --- a/src/test/functional.spec.ts +++ b/src/test/functional.spec.ts @@ -1,5 +1,6 @@ import { expect } from "chai"; import { flatten } from "lodash"; +import { SameType } from "../metaprogramming"; import * as f from "../functional"; @@ -13,6 +14,13 @@ describe("functional", () => { expect([...f.flatten({ a: "b" })]).to.deep.equal([["a", "b"]]); }); + it("Gets the right type for flattening arrays", () => { + const arr = [[["a"], "b"], ["c"]]; + const flattened = [...f.flattenArray(arr)]; + const test: SameType = true; + expect(test).to.be.true; + }); + it("can handle nested objects", () => { const init = { outer: { diff --git a/src/test/metapgrogramming.spec.ts b/src/test/metapgrogramming.spec.ts new file mode 100644 index 00000000000..0ec4cb7faec --- /dev/null +++ b/src/test/metapgrogramming.spec.ts @@ -0,0 +1,76 @@ +import { expect } from "chai"; +import { SameType, RecursiveKeyOf, LeafElems, DeepPick, DeepOmit } from "../metaprogramming"; + +describe("metaprogramming", () => { + it("can calcluate recursive keys", () => { + const test: SameType< + RecursiveKeyOf<{ + a: number; + b: { + c: boolean; + d: { + e: number; + }; + }; + }>, + "a" | "a.b" | "a.b.c" | "a.b.d" | "a.b.d.e" + > = true; + expect(test).to.be.true; + }); + + it("can detect recursive elems", () => { + const test: SameType, "a" | "b" | "c"> = true; + expect(test).to.be.true; + }); + + it("Can deep pick", () => { + interface original { + a: number; + b: { + c: boolean; + d: { + e: number; + }; + g: boolean; + }; + h: number; + } + + interface expected { + a: number; + b: { + c: boolean; + }; + } + + const test: SameType, expected> = true; + expect(test).to.be.true; + }); + + it("can deep omit", () => { + interface original { + a: number; + b: { + c: boolean; + d: { + e: number; + }; + g: boolean; + }; + h: number; + } + + interface expected { + b: { + d: { + e: number; + }; + g: boolean; + }; + h: number; + } + + const test: SameType, expected> = true; + expect(test).to.be.true; + }); +});