Skip to content

Commit d3f860f

Browse files
authored
Introduce some metaprogramming types (#4432)
* Introduce some metaprogramming types * Fix breaks * PR feedback
1 parent f5d73e6 commit d3f860f

File tree

4 files changed

+183
-12
lines changed

4 files changed

+183
-12
lines changed

src/functional.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1+
import { LeafElems } from "./metaprogramming";
2+
13
/**
24
* Flattens an object so that the return value's keys are the path
35
* to a value in the source object. E.g. flattenObject({the: {answer: 42}})
46
* returns {"the.answser": 42}
57
* @param obj An object to be flattened
68
* @return An array where values come from obj and keys are the path in obj to that value.
79
*/
8-
export function* flattenObject(obj: Record<string, unknown>): Generator<[string, unknown]> {
9-
function* helper(path: string[], obj: Record<string, unknown>): Generator<[string, unknown]> {
10+
export function* flattenObject<T extends object>(obj: T): Generator<[string, unknown]> {
11+
function* helper<V extends object>(path: string[], obj: V): Generator<[string, unknown]> {
1012
for (const [k, v] of Object.entries(obj)) {
1113
if (typeof v !== "object" || v === null) {
1214
yield [[...path, k].join("."), v];
1315
} else {
1416
// Object.entries loses type info, so we must cast
15-
yield* helper([...path, k], v as Record<string, unknown>);
17+
yield* helper([...path, k], v);
1618
}
1719
}
1820
}
@@ -25,41 +27,43 @@ export function* flattenObject(obj: Record<string, unknown>): Generator<[string,
2527
* [...flatten([[[1]], [2], 3])] = [1, 2, 3]
2628
*/
2729
// eslint-disable-next-line @typescript-eslint/no-explicit-any
28-
export function* flattenArray<T = any>(arr: unknown[]): Generator<T> {
30+
export function* flattenArray<T extends unknown[]>(arr: T): Generator<LeafElems<T>> {
2931
for (const val of arr) {
3032
if (Array.isArray(val)) {
3133
yield* flattenArray(val);
3234
} else {
33-
yield val as T;
35+
yield val as LeafElems<T>;
3436
}
3537
}
3638
}
3739

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

4446
/** Flattens an object or array. */
45-
export function flatten<T>(
46-
objOrArr: Record<string, unknown> | unknown[]
47-
): Generator<[string, unknown]> | Generator<T> {
47+
export function flatten<T extends unknown[] | object>(objOrArr: T): unknown {
4848
if (Array.isArray(objOrArr)) {
49-
return flattenArray<T>(objOrArr);
49+
return flattenArray(objOrArr);
5050
} else {
5151
return flattenObject(objOrArr);
5252
}
5353
}
5454

55+
type RecursiveElems<T extends unknown[]> = {
56+
[Key in keyof T]: T[Key] extends unknown[] ? T[Key] | RecursiveElems<T[Key]> : T[Key];
57+
}[number];
58+
5559
/**
5660
* Used with reduce to flatten in place.
5761
* Due to the quirks of TypeScript, callers must pass [] as the
5862
* second argument to reduce.
5963
*/
6064
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6165
export function reduceFlat<T = any>(accum: T[] | undefined, next: unknown): T[] {
62-
return [...(accum || []), ...flatten<T>([next])];
66+
return [...(accum || []), ...(flatten([next]) as Generator<T>)];
6367
}
6468

6569
/**

src/metaprogramming.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
type Primitive = string | number | boolean | Function;
2+
3+
/**
4+
* RecursiveKeyOf is a type for keys of an objet usind dots for subfields.
5+
* For a given object: {a: {b: {c: number}}, d } the RecursiveKeysOf are
6+
* 'a' | 'a.b' | 'a.b.c' | 'd'
7+
*/
8+
export type RecursiveKeyOf<T> = T extends Primitive
9+
? never
10+
:
11+
| (keyof T & string)
12+
| {
13+
[P in keyof T & string]: RecursiveSubKeys<T, P>;
14+
}[keyof T & string];
15+
16+
type RecursiveSubKeys<T, P extends keyof T & string> = T[P] extends (infer Elem)[]
17+
? `${P}.${RecursiveKeyOf<Elem>}`
18+
: T[P] extends object
19+
? `${P}.${RecursiveKeyOf<T[P]>}`
20+
: never;
21+
22+
/**
23+
* LeafKeysOf is like RecursiveKeysOf but omits the keys for any object.
24+
* For a given object: {a: {b: {c: number}}, d } the LeafKeysOf are
25+
* 'a.b.c' | 'd'
26+
*/
27+
export type LeafKeysOf<T extends object> = {
28+
[Key in keyof T & (string | number)]: T[Key] extends unknown[]
29+
? `${Key}`
30+
: T[Key] extends object
31+
? `${Key}.${RecursiveKeyOf<T[Key]>}`
32+
: `${Key}`;
33+
}[keyof T & (string | number)];
34+
35+
/**
36+
* SameType is used in testing to verify that two types are the same.
37+
* Usage:
38+
* const test: SameType<A, B> = true.
39+
* The assigment will fail if the types are different.
40+
*/
41+
export type SameType<T, V> = T extends V ? (V extends T ? true : false) : false;
42+
43+
type HeadOf<T extends string> = [T extends `${infer Head}.${infer Tail}` ? Head : T][number];
44+
45+
type TailsOf<T extends string, Head extends string> = [
46+
T extends `${Head}.${infer Tail}` ? Tail : never
47+
][number];
48+
49+
/**
50+
* DeepOmit allows you to omit fields from a nested structure using recursive keys.
51+
*/
52+
export type DeepOmit<T extends object, Keys extends RecursiveKeyOf<T>> = DeepOmitUnsafe<T, Keys>;
53+
54+
type DeepOmitUnsafe<T, Keys extends string> = {
55+
[Key in Exclude<keyof T, Keys>]: Key extends Keys
56+
? T[Key] | undefined
57+
: Key extends HeadOf<Keys>
58+
? DeepOmitUnsafe<T[Key], TailsOf<Keys, Key>>
59+
: T[Key];
60+
};
61+
62+
export type DeepPick<T extends object, Keys extends RecursiveKeyOf<T>> = DeepPickUnsafe<T, Keys>;
63+
64+
type DeepPickUnsafe<T, Keys extends string> = {
65+
[Key in Extract<keyof T, HeadOf<Keys>>]: Key extends Keys
66+
? T[Key]
67+
: DeepPickUnsafe<T[Key], TailsOf<Keys, Key>>;
68+
};
69+
70+
/** In the array LeafElems<[[["a"], "b"], ["c"]]> is "a" | "b" | "c" */
71+
export type LeafElems<T> = T extends Array<infer Elem>
72+
? Elem extends unknown[]
73+
? LeafElems<Elem>
74+
: Elem
75+
: T;
76+
77+
/**
78+
* In the object {a: number, b: { c: string } },
79+
* LeafValues is number | string
80+
*/
81+
export type LeafValues<T extends object> = {
82+
[Key in keyof T]: T[Key] extends object ? LeafValues<T[Key]> : T[Key];
83+
}[keyof T];

src/test/functional.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect } from "chai";
22
import { flatten } from "lodash";
3+
import { SameType } from "../metaprogramming";
34

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

@@ -13,6 +14,13 @@ describe("functional", () => {
1314
expect([...f.flatten({ a: "b" })]).to.deep.equal([["a", "b"]]);
1415
});
1516

17+
it("Gets the right type for flattening arrays", () => {
18+
const arr = [[["a"], "b"], ["c"]];
19+
const flattened = [...f.flattenArray(arr)];
20+
const test: SameType<typeof flattened, string[]> = true;
21+
expect(test).to.be.true;
22+
});
23+
1624
it("can handle nested objects", () => {
1725
const init = {
1826
outer: {

src/test/metapgrogramming.spec.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { expect } from "chai";
2+
import { SameType, RecursiveKeyOf, LeafElems, DeepPick, DeepOmit } from "../metaprogramming";
3+
4+
describe("metaprogramming", () => {
5+
it("can calcluate recursive keys", () => {
6+
const test: SameType<
7+
RecursiveKeyOf<{
8+
a: number;
9+
b: {
10+
c: boolean;
11+
d: {
12+
e: number;
13+
};
14+
};
15+
}>,
16+
"a" | "a.b" | "a.b.c" | "a.b.d" | "a.b.d.e"
17+
> = true;
18+
expect(test).to.be.true;
19+
});
20+
21+
it("can detect recursive elems", () => {
22+
const test: SameType<LeafElems<[[["a"], "b"], ["c"]]>, "a" | "b" | "c"> = true;
23+
expect(test).to.be.true;
24+
});
25+
26+
it("Can deep pick", () => {
27+
interface original {
28+
a: number;
29+
b: {
30+
c: boolean;
31+
d: {
32+
e: number;
33+
};
34+
g: boolean;
35+
};
36+
h: number;
37+
}
38+
39+
interface expected {
40+
a: number;
41+
b: {
42+
c: boolean;
43+
};
44+
}
45+
46+
const test: SameType<DeepPick<original, "a" | "b.c">, expected> = true;
47+
expect(test).to.be.true;
48+
});
49+
50+
it("can deep omit", () => {
51+
interface original {
52+
a: number;
53+
b: {
54+
c: boolean;
55+
d: {
56+
e: number;
57+
};
58+
g: boolean;
59+
};
60+
h: number;
61+
}
62+
63+
interface expected {
64+
b: {
65+
d: {
66+
e: number;
67+
};
68+
g: boolean;
69+
};
70+
h: number;
71+
}
72+
73+
const test: SameType<DeepOmit<original, "a" | "b.c">, expected> = true;
74+
expect(test).to.be.true;
75+
});
76+
});

0 commit comments

Comments
 (0)