Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
33 changes: 30 additions & 3 deletions source/get.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,44 @@ 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<BaseType, Keys, Options extends Required<GetOptions>> =
Keys extends readonly []
? BaseType
: Keys extends readonly [infer Head, ...infer Tail]
? Extract<Head, string> extends keyof BaseType
? GetWithPath<
PropertyOf<BaseType, Extract<Head, string>, Options>,
Extract<Tail, string[]>,
Options
>
: [keyof BaseType & `${Extract<Head, string>}.${string}`] extends [never]
? GetWithPath<
PropertyOf<BaseType, Extract<Head, string>, Options>,
Extract<Tail, string[]>,
Options
>
: GetWithPath_TryDotKey<BaseType, Extract<Head, string>, Extract<Tail, string[]>, 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<BaseType, Prefix extends string, Remaining extends readonly string[], Options extends Required<GetOptions>> =
Remaining extends readonly [infer Next extends string, ...infer Rest extends string[]]
? `${Prefix}.${Next}` extends keyof BaseType
? GetWithPath<
PropertyOf<BaseType, Extract<Head, string>, Options>,
Extract<Tail, string[]>,
PropertyOf<BaseType, `${Prefix}.${Next}`, Options>,
Rest,
Options
>
: never;
: GetWithPath_TryDotKey<BaseType, `${Prefix}.${Next}`, Rest, Options>
: PropertyOf<BaseType, Prefix, Options>;

/**
Adds `undefined` to `Type` if `strict` is enabled.
Expand Down
63 changes: 63 additions & 0 deletions test-d/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,66 @@ expectTypeOf<WithDictionary>().toEqualTypeOf<Get<WithDictionary, readonly []>>()
type FooPaths2 = 'array.1';
expectTypeOf<Get<Foo, FooPaths2>>().toEqualTypeOf<string | undefined>();
}

// 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<Get<WithDotKeys, 'test1.test2'>>().toEqualTypeOf<{test3: {test4: number}}>();
expectTypeOf<Get<WithDotKeys, 'test1.test2.test3'>>().toEqualTypeOf<{test4: number}>();
expectTypeOf<Get<WithDotKeys, 'test1.test2.test3.test4'>>().toEqualTypeOf<number>();

expectTypeOf<Get<WithDotKeys, 'test1.test2', NonStrict>>().toEqualTypeOf<{test3: {test4: number}}>();
expectTypeOf<Get<WithDotKeys, 'test1.test2.test3', NonStrict>>().toEqualTypeOf<{test4: number}>();
expectTypeOf<Get<WithDotKeys, 'test1.test2.test3.test4', NonStrict>>().toEqualTypeOf<number>();

// Array path should treat the dotted key as a single element
expectTypeOf<Get<WithDotKeys, ['test1.test2']>>().toEqualTypeOf<{test3: {test4: number}}>();
expectTypeOf<Get<WithDotKeys, ['test1.test2', 'test3', 'test4']>>().toEqualTypeOf<number>();

// Non-existent paths should still return unknown
expectTypeOf<Get<WithDotKeys, 'test1', NonStrict>>().toBeUnknown();
expectTypeOf<Get<WithDotKeys, 'test1.test2.nonexistent', NonStrict>>().toBeUnknown();

// Key with multiple dots
type WithMultiDotKey = {
['test1.test2.test3']: {
test4: number;
};
};

expectTypeOf<Get<WithMultiDotKey, 'test1.test2.test3'>>().toEqualTypeOf<{test4: number}>();
expectTypeOf<Get<WithMultiDotKey, 'test1.test2.test3.test4'>>().toEqualTypeOf<number>();
expectTypeOf<Get<WithMultiDotKey, 'test1.test2.test3', NonStrict>>().toEqualTypeOf<{test4: number}>();
expectTypeOf<Get<WithMultiDotKey, 'test1.test2.test3.test4', NonStrict>>().toEqualTypeOf<number>();
expectTypeOf<Get<WithMultiDotKey, 'test1.test2', NonStrict>>().toBeUnknown();
expectTypeOf<Get<WithMultiDotKey, 'test1', NonStrict>>().toBeUnknown();

// Multiple levels of dot-containing keys
type NestedDotKeys = {
['a.b']: {
['c.d']: {
value: string;
};
};
};

expectTypeOf<Get<NestedDotKeys, 'a.b.c.d.value', NonStrict>>().toBeString();
expectTypeOf<Get<NestedDotKeys, 'a.b.c.d', NonStrict>>().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<Get<Ambiguous, 'a.b', NonStrict>>().toBeNumber();
expectTypeOf<Get<Ambiguous, ['a.b'], NonStrict>>().toBeString();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Suggested change
expectTypeOf<Get<Ambiguous, ['a.b'], NonStrict>>().toBeString();
expectTypeOf<Get<Ambiguous, ['a.b'], NonStrict>>().toBeString();
// Prefer the longest exact dotted key match when multiple dotted keys overlap
type OverlappingDotKeys = {
'a.b': string;
'a.b.c': number;
};
expectTypeOf<Get<OverlappingDotKeys, 'a.b.c', NonStrict>>().toBeNumber();
expectTypeOf<Get<OverlappingDotKeys, ['a.b.c'], NonStrict>>().toBeNumber();
// Longer exact dotted key should continue resolving the remaining path
type OverlappingDotKeysNested = {
'a.b': {
c: string;
};
'a.b.c': {
d: boolean;
};
};
expectTypeOf<Get<OverlappingDotKeysNested, 'a.b.c.d', NonStrict>>().toBeBoolean();
expectTypeOf<Get<OverlappingDotKeysNested, ['a.b.c', 'd'], NonStrict>>().toBeBoolean();

}