-
Notifications
You must be signed in to change notification settings - Fork 54
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
Add Dot Notation key support to utility functions #2256
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,11 @@ | ||
import type { TypeOfTag } from "typescript/lib/typescript"; | ||
import type { | ||
DotNotationKeys, | ||
DotNotationObject, | ||
FlatObject, | ||
GetValueFromDotKey, | ||
InputValue | ||
} from "../../../types/helperTypes"; | ||
|
||
/** | ||
* Benchmark the performance of a function, calling it a requested number of iterations. | ||
|
@@ -172,6 +179,7 @@ export declare function encodeURL(path: string): string; | |
* (default: `0`) | ||
* @returns An expanded object | ||
*/ | ||
export declare function expandObject<T extends Record<string, unknown>>(obj: FlatObject<T>, _d?: number): T; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The inference here isn't going to be ideal here, I think, based upon my prior experience with "inverse inference" -- where to infer expectType<{ foo: { bar: 1 } }>(expandObject({ "foo.bar": 1 }));
expectType<{ foo: { bar: 1 } }>(expandObject({ "foo.bar": 1, "foo.lorem": 2 }));
expectType<{ foo: { bar: 1, lorem: 2 } }>(expandObject({ "foo": { "bar": 1 }, "foo.lorem": "2" })); We can get this draft in first and I can add in an |
||
export declare function expandObject(obj: object, _d?: number): any; | ||
|
||
/** | ||
|
@@ -213,6 +221,7 @@ interface FilterObjectOptions { | |
* @param d - Track the recursion depth to prevent overflow | ||
* @returns A flattened object | ||
*/ | ||
export declare function flattenObject<T extends Record<string, unknown>>(obj: T, _d?: number): FlatObject<T>; | ||
export declare function flattenObject(obj: object, _d?: number): any; | ||
|
||
/** | ||
|
@@ -256,7 +265,7 @@ export function getType( | |
* @param key - An object property with notation a.b.c | ||
* @returns An indicator for whether the property exists | ||
*/ | ||
export declare function hasProperty(object: object, key: string): boolean; | ||
export declare function hasProperty<T extends InputValue, key extends DotNotationKeys<T>>(object: T, key: key): boolean; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit; type parameters in this repo should be PascalCase so it should be |
||
|
||
/** | ||
* A helper function which searches through an object to retrieve a value by a string key. | ||
|
@@ -265,7 +274,10 @@ export declare function hasProperty(object: object, key: string): boolean; | |
* @param key - An object property with notation a.b.c | ||
* @returns The value of the found property | ||
*/ | ||
export declare function getProperty(object: object, key: string): any; | ||
export declare function getProperty<T extends InputValue, K extends DotNotationKeys<T>>( | ||
object: T, | ||
key: K | ||
): GetValueFromDotKey<T, K>; | ||
|
||
/** | ||
* A helper function which searches through an object to assign a value using a string key | ||
|
@@ -275,7 +287,11 @@ export declare function getProperty(object: object, key: string): any; | |
* @param value - The value to be assigned | ||
* @returns Whether the value was changed from its previous value | ||
*/ | ||
export declare function setProperty(object: object, key: string, value: any): boolean; | ||
export declare function setProperty<T extends InputValue, K extends DotNotationKeys<T>>( | ||
object: T, | ||
key: K, | ||
value: GetValueFromDotKey<T, K> | ||
): boolean; | ||
|
||
/** | ||
* Invert an object by assigning its values as keys and its keys as values. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -161,3 +161,95 @@ export type DataSourceForPlaceable<P extends PlaceableObject> = P extends Placea | |
? D["_source"] | ||
: never | ||
: never; | ||
|
||
type EndValue = string | bigint | number | boolean | null | undefined; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would call this |
||
export type InputValue = object | Array<unknown>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
type Split<S extends string, D extends string> = string extends S | ||
? string[] | ||
: S extends "" | ||
? [] | ||
: S extends `${infer T}${D}${infer U}` | ||
? [T, ...Split<U, D>] | ||
: [S]; | ||
|
||
type JoinParts<T extends (string | number)[], D extends string> = T extends [] | ||
? never | ||
: T extends [infer F] | ||
? `${F & (number | string)}` | ||
: T extends [infer F, ...infer R] | ||
? F extends EndValue | ||
? `${F}${D}${JoinParts<Extract<R, (string | number)[]>, D>}` | ||
: never | ||
: string; | ||
// TODO: fix for TS4.8 for tuples, string to number literal cast | ||
export type ArrayOrTupleKey<T> = StringNumber<keyof T & string> extends never ? number : StringNumber<keyof T & string>; | ||
|
||
type GetPathParts<T, LeafsOnly extends boolean = false> = T extends EndValue | ||
? [] | ||
: Exclude< | ||
T extends Array<unknown> | ||
? | ||
| { | ||
[K in ArrayOrTupleKey<T>]: [K, ...GetPathParts<T[K], LeafsOnly>]; | ||
}[ArrayOrTupleKey<T>] | ||
| (LeafsOnly extends true ? never : [ArrayOrTupleKey<T>]) | ||
: | ||
| { | ||
[K in Extract<keyof T, EndValue>]: [K, ...GetPathParts<T[K], LeafsOnly>]; | ||
}[Extract<keyof T, EndValue>] | ||
| (LeafsOnly extends true ? never : [Extract<keyof T, EndValue>]), | ||
never | ||
>; | ||
|
||
type GetPathPartsObjectOnly<T extends object> = T extends EndValue | ||
? [] | ||
: Exclude< | ||
{ | ||
[K in Extract<keyof T, EndValue>]: [K, ...GetPathParts<T[K]>]; | ||
}[Extract<keyof T, EndValue>], | ||
never | ||
>; | ||
|
||
export type GetValueFromDotKey<T, P extends string> = InnerGetValueFromDotKey<T, Split<P, ".">>; | ||
type InnerGetValueFromDotKey<T, P extends string[]> = T extends Array<unknown> | ||
? InnerGetValueFromDotKeyArray<T, P> | ||
: T extends object | ||
? InnerGetValueFromDotKeyObject<T, P> | ||
: never; | ||
// TODO: fix for TS4.8 for tuples, string to number literal cast | ||
type InnerGetValueFromDotKeyArray<T extends Array<unknown>, P extends string[]> = P extends [string] | ||
? T[number] | ||
: P extends [string, ...infer R] | ||
? InnerGetValueFromDotKey<T[number], Extract<R, string[]>> | ||
: never; | ||
type InnerGetValueFromDotKeyObject<T extends object, P extends string[]> = P extends [infer K] | ||
? T[K & keyof T] | ||
: P extends [keyof T, ...infer R] | ||
? InnerGetValueFromDotKey<T[P[0] & keyof T], Extract<R, string[]>> | ||
: never; | ||
|
||
/** @internal exported for tests */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would say you should just test this by checking |
||
export type DotNotationKeys<T extends InputValue, LeafsOnly extends boolean = false> = JoinParts< | ||
GetPathParts<T, LeafsOnly>, | ||
"." | ||
>; | ||
|
||
export type DotNotationObject<T extends InputValue> = { | ||
[K in DotNotationKeys<T>]: GetValueFromDotKey<T, K>; | ||
}; | ||
|
||
export type FlatObject<T extends Record<string, unknown>> = { | ||
[K in JoinParts<GetPathPartsObjectOnly<T>, ".">]: GetValueFromDotKey<T, K>; | ||
}; | ||
|
||
type AddDeletionMarkToLastPart<T> = T extends [...infer L, infer B] | ||
? L extends (string | number)[] | ||
? [...L, `-=${B & (string | number)}`] | ||
: never | ||
: never; | ||
/** @internal exported for tests */ | ||
export type DeleteKeys<T> = JoinParts<AddDeletionMarkToLastPart<GetPathParts<T>>, ".">; | ||
export type DeletionUpdate<T extends InputValue> = { | ||
[K in DeleteKeys<T> | DotNotationKeys<T>]?: K extends DeleteKeys<T> ? unknown : GetValueFromDotKey<T, K>; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -141,3 +141,5 @@ type PropertyTypeOrFallback<T, Key extends string, Fallback> = Key extends keyof | |
* Makes the given keys `K` of the type `T` required | ||
*/ | ||
type RequiredProps<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>; | ||
|
||
type StringNumber<S extends string> = S extends `${number}` ? S : never; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd call this |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -202,6 +202,44 @@ declare class ClassWithConstructorParameters { | |
expectType<boolean>(foundry.utils.isSubclass(ClassWithNoConstructorParameters, ClassWithConstructorParameters)); | ||
expectType<boolean>(foundry.utils.isSubclass(ClassWithConstructorParameters, ClassWithNoConstructorParameters)); | ||
|
||
// expandObject | ||
// hard to infer back | ||
expectType<{}>(foundry.utils.expandObject({})); | ||
expectType<{ test: number }>(foundry.utils.expandObject({ test: 1 })); | ||
expectType<{ test: number; deep: { value: number } }>( | ||
foundry.utils.expandObject<{ test: number; deep: { value: number } }>({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honestly this test isn't really testing anything to do with |
||
test: 1, | ||
"deep.value": 1 | ||
}) | ||
); | ||
expectType<{ "0": number }>(foundry.utils.expandObject<{ "0": number }>({ "0": 1 })); | ||
|
||
// hasProperty | ||
expectType<boolean>(foundry.utils.hasProperty({ k1: 1 }, "k1")); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would just note that |
||
expectError(foundry.utils.hasProperty({ k1: 1 }, "k2")); | ||
expectType<boolean>(foundry.utils.hasProperty({ deep: { value: 1 } }, "deep")); | ||
expectType<boolean>(foundry.utils.hasProperty({ deep: { value: 1 } }, "deep.value")); | ||
expectError(foundry.utils.hasProperty({ deep: { key: 1 } }, "deep.value")); | ||
|
||
// getProperty | ||
expectType<number>(foundry.utils.getProperty({ k1: 1, k2: "a" }, "k1")); | ||
expectType<string>(foundry.utils.getProperty({ k1: 1, k2: "a" }, "k2")); | ||
expectError(foundry.utils.getProperty({ k1: 1, k2: "a" }, "k3")); | ||
expectType<{ value: number }>(foundry.utils.getProperty({ deep: { value: 1 } }, "deep")); | ||
expectType<number>(foundry.utils.getProperty({ deep: { value: 1 } }, "deep.value")); | ||
expectType<1>(foundry.utils.getProperty({ deep: { value: 1 as const } }, "deep.value")); | ||
|
||
// setProperty | ||
expectType<boolean>(foundry.utils.setProperty({ k1: 1, k2: "a" }, "k1", 2)); | ||
expectType<boolean>(foundry.utils.setProperty({ k1: 1, k2: "a" }, "k2", "b")); | ||
expectError(foundry.utils.setProperty({ k1: 1, k2: "a" }, "k3", null)); | ||
expectType<boolean>(foundry.utils.setProperty({ deep: { value: 0 } }, "deep", { value: 1 })); | ||
expectType<boolean>(foundry.utils.setProperty({ deep: { value: 0 } }, "deep.value", 1)); | ||
expectType<boolean>(foundry.utils.setProperty({ deep: { value: 1 as 0 | 1 | -1 } }, "deep.value", -1)); | ||
expectError(foundry.utils.setProperty({ deep: { value: 1 as 0 | 1 | -1 } }, "deep.value", 2)); | ||
expectType<boolean>(foundry.utils.setProperty({ other: 1 } as Record<string, number>, "test", 3)); | ||
expectType<boolean>(foundry.utils.setProperty({ other: 1 } as Record<"test" | "other", number>, "test", 3)); | ||
|
||
// invertObject | ||
expectType<{ readonly 1: "a"; readonly foo: "b" }>(foundry.utils.invertObject({ a: 1, b: "foo" } as const)); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { expectType } from "tsd"; | ||
import type { ArrayOrTupleKey, DotNotationKeys, GetValueFromDotKey } from "../../src/types/helperTypes"; | ||
|
||
declare function type<T>(): T; | ||
|
||
// basic objects | ||
expectType<"k1">(type<DotNotationKeys<{ k1: 1 }>>()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While it's not a bad idea to start testing our types I think the import { TypeEqual } from "ts-expect";
expectType<TypeEqual<DotNotationKeys{ k1: 1 }>, "k1">(true); It's still a a bit awkward but it's at least more expressive and something shown in |
||
expectType<"k2">(type<DotNotationKeys<{ k2: 1 }>>()); | ||
expectType<"k1" | "k2">(type<DotNotationKeys<{ k1: 1; k2: 1 }>>()); | ||
|
||
// objects with depth | ||
expectType<"k1" | "k2" | "deep" | "deep.value">(type<DotNotationKeys<{ k1: 1; k2: 1; deep: { value: 1 } }>>()); | ||
expectType< | ||
| "a" | ||
| "a.really" | ||
| "a.really.deep" | ||
| "a.really.deep.object" | ||
| "a.really.deep.object.holding" | ||
| "a.really.deep.object.holding.a" | ||
| "a.really.deep.object.holding.a.value" | ||
>(type<DotNotationKeys<{ a: { really: { deep: { object: { holding: { a: { value: 1 } } } } } } }>>()); | ||
|
||
// arrays | ||
expectType<`${number}`>(type<DotNotationKeys<number[]>>()); | ||
expectType<`${number}` | `${number}.${number}`>(type<DotNotationKeys<number[][]>>()); | ||
expectType<`${number}` | `${number}.${number}` | `${number}.${number}.${number}`>( | ||
type<DotNotationKeys<number[][][]>>() | ||
); | ||
|
||
// tuples! | ||
expectType<"0">(type<ArrayOrTupleKey<[9]>>()); | ||
expectType<"0" | "1">(type<ArrayOrTupleKey<[9, 9]>>()); | ||
expectType<number>(type<ArrayOrTupleKey<9[]>>()); | ||
expectType<"0">(type<DotNotationKeys<[9]>>()); | ||
expectType<"0" | "1">(type<DotNotationKeys<[9, 9]>>()); | ||
expectType<"0" | "0.int" | "0.str">(type<DotNotationKeys<[{ int: 1; str: "" }]>>()); | ||
expectType<"0" | "0.int" | "0.str" | "1">(type<DotNotationKeys<[{ int: 1; str: "" }, 2]>>()); | ||
|
||
// arrays with objects | ||
expectType<`${number}` | `${number}.k1`>(type<DotNotationKeys<{ k1: 1 }[]>>()); | ||
expectType<`${number}` | `${number}.k2`>(type<DotNotationKeys<{ k2: 2 }[]>>()); | ||
expectType<`${number}` | `${number}.k1` | `${number}.k2`>(type<DotNotationKeys<{ k1: 1; k2: 2 }[]>>()); | ||
|
||
// objects with arrays | ||
expectType<`array` | `array.${number}`>(type<DotNotationKeys<{ array: number[] }>>()); | ||
expectType<`array` | `array.${number}` | `deep` | `deep.array` | `deep.array.${number}`>( | ||
type<DotNotationKeys<{ array: number[]; deep: { array: number[] } }>>() | ||
); | ||
|
||
// objects with tuples | ||
expectType<"tuple" | "tuple.0" | "tuple.1" | "tuple.0.name" | "tuple.1.name">( | ||
type<DotNotationKeys<{ tuple: [{ name: string }, { name: string }] }>>() | ||
); | ||
|
||
// mixed | ||
expectType<"tuple" | "tuple.0" | "tuple.1" | "tuple.2" | "array" | `array.${number}` | "deep" | "deep.value">( | ||
type<DotNotationKeys<{ tuple: [1, 2, 3]; array: number[]; deep: { value: number } }>>() | ||
); | ||
expectType<"0" | "1" | `1.${number}` | "2" | "2.value">(type<DotNotationKeys<[1, number[], { value: number }]>>()); | ||
expectType<`${number}` | `${number}.0` | `${number}.1`>(type<DotNotationKeys<[number, number][]>>()); | ||
|
||
expectType<number>(type<GetValueFromDotKey<{ value: number }, "value">>()); | ||
expectType<number>(type<GetValueFromDotKey<[number, 1], "0">>()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As GitHub Actions lints this import is unused.