Skip to content

Introduce some metaprogramming types #4432

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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];

Comment on lines +55 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think we don't need this anymore.

/**
* 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

woo such usefulness.


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;
});
});