diff --git a/source/get.d.ts b/source/get.d.ts index e1077ded1..d8908fa73 100644 --- a/source/get.d.ts +++ b/source/get.d.ts @@ -22,17 +22,42 @@ type DefaultGetOptions = { /** Like the `Get` type but receives an array of strings as a path parameter. + +When a key segment doesn't directly match a property and the type has keys containing dots that start with that segment, it tries progressively joining adjacent segments with dots to match those keys (e.g. `{'foo.bar': value}`). */ type GetWithPath> = Keys extends readonly [] ? BaseType : Keys extends readonly [infer Head, ...infer Tail] + ? Extract extends keyof BaseType + ? GetWithPath< + PropertyOf, Options>, + Extract, + Options + > + : [keyof BaseType & `${Extract}.${string}`] extends [never] + ? GetWithPath< + PropertyOf, Options>, + Extract, + Options + > + : GetWithPath_TryDotKey, Extract, Options> + : never; + +/** +Try progressively longer dot-joined key prefixes to handle object keys that contain dots. +Falls back to `PropertyOf` for the original key when no dotted key matches. +*/ +type GetWithPath_TryDotKey> = + Remaining extends readonly [infer Next extends string, ...infer Rest extends string[]] + ? `${Prefix}.${Next}` extends keyof BaseType ? GetWithPath< - PropertyOf, Options>, - Extract, + PropertyOf, + Rest, Options > - : never; + : GetWithPath_TryDotKey + : PropertyOf; /** Adds `undefined` to `Type` if `strict` is enabled. diff --git a/test-d/get.ts b/test-d/get.ts index 8b13a941d..5e8dc4700 100644 --- a/test-d/get.ts +++ b/test-d/get.ts @@ -152,3 +152,66 @@ expectTypeOf().toEqualTypeOf>() type FooPaths2 = 'array.1'; expectTypeOf>().toEqualTypeOf(); } + +// Test keys containing dots (https://github.com/sindresorhus/type-fest/issues/1372) +// eslint-disable-next-line no-lone-blocks +{ + type WithDotKeys = { + ['test1.test2']: { + test3: { + test4: number; + }; + }; + }; + + expectTypeOf>().toEqualTypeOf<{test3: {test4: number}}>(); + expectTypeOf>().toEqualTypeOf<{test4: number}>(); + expectTypeOf>().toEqualTypeOf(); + + expectTypeOf>().toEqualTypeOf<{test3: {test4: number}}>(); + expectTypeOf>().toEqualTypeOf<{test4: number}>(); + expectTypeOf>().toEqualTypeOf(); + + // Array path should treat the dotted key as a single element + expectTypeOf>().toEqualTypeOf<{test3: {test4: number}}>(); + expectTypeOf>().toEqualTypeOf(); + + // Non-existent paths should still return unknown + expectTypeOf>().toBeUnknown(); + expectTypeOf>().toBeUnknown(); + + // Key with multiple dots + type WithMultiDotKey = { + ['test1.test2.test3']: { + test4: number; + }; + }; + + expectTypeOf>().toEqualTypeOf<{test4: number}>(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf<{test4: number}>(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toBeUnknown(); + expectTypeOf>().toBeUnknown(); + + // Multiple levels of dot-containing keys + type NestedDotKeys = { + ['a.b']: { + ['c.d']: { + value: string; + }; + }; + }; + + expectTypeOf>().toBeString(); + expectTypeOf>().toEqualTypeOf<{value: string}>(); + + // When both a dotted key and a nested path exist, prefer the nested path (matches JS dot-access semantics) + type Ambiguous = { + a: {b: number}; + 'a.b': string; + }; + + expectTypeOf>().toBeNumber(); + expectTypeOf>().toBeString(); +}