Last active
August 1, 2024 16:06
-
-
Save Quramy/d2a3b6eb5a5edf469fea0546f1118561 to your computer and use it in GitHub Desktop.
Util type to pick object recursively
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace type_test_for_DeepPick { | |
type BlogPost = { | |
id: string; | |
isDraft: boolean; | |
title: string; | |
subTitle?: string; | |
body: string; | |
author?: { | |
id: string; | |
firstName: string; | |
lastName: string; | |
createdAt: string; | |
updatedAt: string; | |
}; | |
tags?: { | |
id: string; | |
color: string; | |
label: string; | |
createdAt: string; | |
updatedAt: string; | |
}[]; | |
createdAt: string; | |
updatedAt: string; | |
}; | |
namespace example { | |
const testObj: DeepPick<BlogPost, "id" | "author.id" | "author.firstName" | "tags.label"> = { | |
id: "b0001", | |
author: { | |
id: "user001", | |
firstName: "Bob", | |
}, | |
tags: [{ label: "Programming" }, { label: "TypeScript" }], | |
}; | |
// @ts-expect-error | |
type WithInvalidPath = DeepPick<BlogPost, "id" | "not_existing_key">; | |
} | |
namespace simple_path { | |
const testObj: DeepPick<BlogPost, "id" | "title" | "author"> = { | |
id: "b0001", | |
title: "awesome blog", | |
author: { | |
id: "user001", | |
firstName: "Bob", | |
lastName: "Anderson", | |
createdAt: "2020-01-01T09:00:00.000Z", | |
updatedAt: "2020-01-01T09:00:00.000Z", | |
}, | |
}; | |
const testObjAlt = testObj satisfies Pick<BlogPost, "id" | "title" | "author">; | |
} | |
namespace path_for_nested_record { | |
const testObj: DeepPick<BlogPost, "id" | "author.id" | "author.firstName"> = { | |
id: "b0001", | |
author: { | |
id: "user001", | |
firstName: "Bob", | |
}, | |
}; | |
} | |
namespace path_for_nested_array { | |
const testObj: DeepPick<BlogPost, "id" | "tags.id" | "tags.color"> = { | |
id: "b0001", | |
tags: [ | |
{ | |
id: "tag001", | |
color: "#ff0000", | |
}, | |
], | |
}; | |
const testObjAlt: DeepPick<BlogPost, "id" | "tags.id" | "tags.color"> = { | |
id: "b0001", | |
tags: undefined, | |
}; | |
} | |
namespace type_test_for_PathForDeepPick { | |
type CircularRefType = { id: string; children?: CircularRefType[] }; | |
type TestPathCircularRefType = PathForDeepPick<CircularRefType>; // string | |
} | |
} | |
type _s = symbol; | |
declare namespace Nat { | |
export type Nat = readonly _s[]; | |
export type Zero = []; | |
export type Increment<Value extends Nat> = [_s, ...Value]; | |
export type ToNumber<Value extends Nat> = Value["length"]; | |
export type From<N extends number> = N extends 0 | |
? Zero | |
: N extends 1 | |
? [_s] | |
: N extends 2 | |
? [_s, _s] | |
: N extends 3 | |
? [_s, _s, _s] | |
: N extends 4 | |
? [_s, _s, _s, _s] | |
: N extends 5 | |
? [_s, _s, _s, _s, _s] | |
: N extends 6 | |
? [_s, _s, _s, _s, _s, _s] | |
: never; | |
} | |
type DottedChildrenPath<Path extends string, ParentKey extends string> = Path extends ParentKey | |
? string | |
: Path extends `${ParentKey}.${infer ChildPath}` | |
? ChildPath | |
: never; | |
type DottedParentKey<Path extends string> = Path extends `${infer ParentKey}.${string}` | |
? ParentKey | |
: Path; | |
type MaxRecursionDepth = Nat.From<4>; | |
type DottedPath< | |
T, | |
ParentKey extends string = "", | |
RecursionDepth extends Nat.Nat = Nat.Zero, | |
> = RecursionDepth extends MaxRecursionDepth | |
? string | |
: T extends readonly (infer TItem)[] | |
? DottedPath<TItem, ParentKey, Nat.Increment<RecursionDepth>> | |
: T extends Record<string, unknown> | |
? Exclude< | |
{ | |
[Key in keyof T]: Key extends string | |
? DottedPath< | |
T[Key], | |
ParentKey extends "" ? Key : `${ParentKey}.${Key}`, | |
Nat.Increment<RecursionDepth> | |
> | |
: never; | |
}[keyof T], | |
undefined | |
> | |
: ParentKey; | |
type Primitive = undefined | null | boolean | number | string | bigint | symbol; | |
type ValueOrRecursivePick<TValue, Path extends string> = TValue extends Primitive | |
? TValue | |
: TValue extends readonly (infer TItem)[] | |
? ValueOrRecursivePick<TItem, Path>[] | |
: TValue extends Record<string, unknown> | |
? DeepPickInternal<TValue, Path> | |
: never; | |
type DeepPickInternal<TObject extends Record<string, unknown>, Path extends string> = { | |
[Key in Extract<keyof TObject, DottedParentKey<Path>>]: ValueOrRecursivePick< | |
TObject[Key], | |
DottedChildrenPath<Path, Key> | |
>; | |
}; | |
export type PathForDeepPick<T> = DottedPath<T, "">; | |
export type DeepPick< | |
TObject extends Record<string, unknown>, | |
Path extends PathForDeepPick<TObject>, | |
> = DeepPickInternal<TObject, Path>; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment