Skip to content

Commit

Permalink
Introduce some metaprogramming types (#4432)
Browse files Browse the repository at this point in the history
* Introduce some metaprogramming types

* Fix breaks

* PR feedback
  • Loading branch information
inlined authored Apr 17, 2022
1 parent f5d73e6 commit d3f860f
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 12 deletions.
28 changes: 16 additions & 12 deletions src/functional.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
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}})
* returns {"the.answser": 42}
* @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<string, unknown>): Generator<[string, unknown]> {
function* helper(path: string[], obj: Record<string, unknown>): Generator<[string, unknown]> {
export function* flattenObject<T extends object>(obj: T): Generator<[string, unknown]> {
function* helper<V extends object>(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<string, unknown>);
yield* helper([...path, k], v);
}
}
}
Expand All @@ -25,41 +27,43 @@ export function* flattenObject(obj: Record<string, unknown>): Generator<[string,
* [...flatten([[[1]], [2], 3])] = [1, 2, 3]
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function* flattenArray<T = any>(arr: unknown[]): Generator<T> {
export function* flattenArray<T extends unknown[]>(arr: T): Generator<LeafElems<T>> {
for (const val of arr) {
if (Array.isArray(val)) {
yield* flattenArray(val);
} else {
yield val as T;
yield val as LeafElems<T>;
}
}
}

/** Shorthand for flattenObject. */
export function flatten(obj: Record<string, unknown>): Generator<[string, unknown]>;
export function flatten<T extends object>(obj: T): Generator<[string, string]>;
/** Shorthand for flattenArray. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function flatten<T = any>(arr: unknown[]): Generator<T>;
export function flatten<T extends unknown[]>(arr: T): Generator<LeafElems<T>>;

/** Flattens an object or array. */
export function flatten<T>(
objOrArr: Record<string, unknown> | unknown[]
): Generator<[string, unknown]> | Generator<T> {
export function flatten<T extends unknown[] | object>(objOrArr: T): unknown {
if (Array.isArray(objOrArr)) {
return flattenArray<T>(objOrArr);
return flattenArray(objOrArr);
} else {
return flattenObject(objOrArr);
}
}

type RecursiveElems<T extends unknown[]> = {
[Key in keyof T]: T[Key] extends unknown[] ? T[Key] | RecursiveElems<T[Key]> : T[Key];
}[number];

/**
* Used with reduce to flatten in place.
* Due to the quirks of TypeScript, callers must pass [] as the
* second argument to reduce.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function reduceFlat<T = any>(accum: T[] | undefined, next: unknown): T[] {
return [...(accum || []), ...flatten<T>([next])];
return [...(accum || []), ...(flatten([next]) as Generator<T>)];
}

/**
Expand Down
83 changes: 83 additions & 0 deletions src/metaprogramming.ts
Original file line number Diff line number Diff line change
@@ -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> = T extends Primitive
? never
:
| (keyof T & string)
| {
[P in keyof T & string]: RecursiveSubKeys<T, P>;
}[keyof T & string];

type RecursiveSubKeys<T, P extends keyof T & string> = T[P] extends (infer Elem)[]
? `${P}.${RecursiveKeyOf<Elem>}`
: T[P] extends object
? `${P}.${RecursiveKeyOf<T[P]>}`
: 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<T extends object> = {
[Key in keyof T & (string | number)]: T[Key] extends unknown[]
? `${Key}`
: T[Key] extends object
? `${Key}.${RecursiveKeyOf<T[Key]>}`
: `${Key}`;
}[keyof T & (string | number)];

/**
* SameType is used in testing to verify that two types are the same.
* Usage:
* const test: SameType<A, B> = true.
* The assigment will fail if the types are different.
*/
export type SameType<T, V> = T extends V ? (V extends T ? true : false) : false;

type HeadOf<T extends string> = [T extends `${infer Head}.${infer Tail}` ? Head : T][number];

type TailsOf<T extends string, Head extends string> = [
T extends `${Head}.${infer Tail}` ? Tail : never
][number];

/**
* DeepOmit allows you to omit fields from a nested structure using recursive keys.
*/
export type DeepOmit<T extends object, Keys extends RecursiveKeyOf<T>> = DeepOmitUnsafe<T, Keys>;

type DeepOmitUnsafe<T, Keys extends string> = {
[Key in Exclude<keyof T, Keys>]: Key extends Keys
? T[Key] | undefined
: Key extends HeadOf<Keys>
? DeepOmitUnsafe<T[Key], TailsOf<Keys, Key>>
: T[Key];
};

export type DeepPick<T extends object, Keys extends RecursiveKeyOf<T>> = DeepPickUnsafe<T, Keys>;

type DeepPickUnsafe<T, Keys extends string> = {
[Key in Extract<keyof T, HeadOf<Keys>>]: Key extends Keys
? T[Key]
: DeepPickUnsafe<T[Key], TailsOf<Keys, Key>>;
};

/** In the array LeafElems<[[["a"], "b"], ["c"]]> is "a" | "b" | "c" */
export type LeafElems<T> = T extends Array<infer Elem>
? Elem extends unknown[]
? LeafElems<Elem>
: Elem
: T;

/**
* In the object {a: number, b: { c: string } },
* LeafValues is number | string
*/
export type LeafValues<T extends object> = {
[Key in keyof T]: T[Key] extends object ? LeafValues<T[Key]> : T[Key];
}[keyof T];
8 changes: 8 additions & 0 deletions src/test/functional.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect } from "chai";
import { flatten } from "lodash";
import { SameType } from "../metaprogramming";

import * as f from "../functional";

Expand All @@ -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<typeof flattened, string[]> = true;
expect(test).to.be.true;
});

it("can handle nested objects", () => {
const init = {
outer: {
Expand Down
76 changes: 76 additions & 0 deletions src/test/metapgrogramming.spec.ts
Original file line number Diff line number Diff line change
@@ -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<LeafElems<[[["a"], "b"], ["c"]]>, "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<DeepPick<original, "a" | "b.c">, 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<DeepOmit<original, "a" | "b.c">, expected> = true;
expect(test).to.be.true;
});
});

0 comments on commit d3f860f

Please sign in to comment.