Skip to content

Commit 42627d1

Browse files
authored
Merge pull request #28 from kakasoo/kakasoo/fix-tuple-keys
fix: support tuple types in DeepStrictObjectKeys
2 parents d41e50d + 83578ce commit 42627d1

File tree

2 files changed

+97
-2
lines changed

2 files changed

+97
-2
lines changed

src/types/DeepStrictObjectKeys.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ import type { IsUnion } from './IsUnion';
55
import type { ValueType } from './ValueType';
66

77
namespace DeepStrictObjectKeys {
8+
/**
9+
* Distributive wrapper for Infer that properly handles union element types.
10+
* When Element is a union (e.g., {a: 1} | {b: 2} from tuple [{ a: 1 }, { b: 2 }]),
11+
* this distributes the Infer call to each member of the union individually,
12+
* avoiding the issue where keyof (A | B) = (keyof A) & (keyof B) = never.
13+
*/
14+
type DistributeInfer<T, Joiner extends { array: string; object: string }, IsSafe extends boolean> = T extends object
15+
? Infer<T, Joiner, IsSafe>
16+
: never;
17+
818
/**
919
* Internal helper type that recursively extracts all keys from nested objects
1020
* @template Target - The object type to extract keys from
@@ -34,7 +44,7 @@ namespace DeepStrictObjectKeys {
3444
? // For arrays of objects, add array notation and recurse into elements
3545
| P
3646
// | (Equal<IsSafe, true> extends true ? never : `${P}[*]`) // end of array
37-
| `${P}${Joiner['array']}${Joiner['object']}${Infer<_Element, Joiner, IsSafe>}` // recursive
47+
| `${P}${Joiner['array']}${Joiner['object']}${DistributeInfer<_Element, Joiner, IsSafe>}` // recursive
3848
: // For regular objects, add object notation and recurse
3949
`${P}${Joiner['object']}${Infer<E, Joiner, IsSafe>}` // recursive
4050
: never // Remove all primitive types of union types.
@@ -43,7 +53,7 @@ namespace DeepStrictObjectKeys {
4353
? // Handle arrays containing objects
4454
| P
4555
// | (Equal<IsSafe, true> extends true ? never : `${P}[*]`) // end of array
46-
| `${P}${Joiner['array']}${Joiner['object']}${Infer<Element, Joiner, false>}`
56+
| `${P}${Joiner['array']}${Joiner['object']}${DistributeInfer<Element, Joiner, false>}`
4757
: Target[P] extends Array<infer _Element>
4858
? // Handle arrays containing primitives
4959
Equal<IsSafe, true> extends true

test/features/DeepStrictObjectKeys.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,3 +966,88 @@ export function test_types_deep_strict_object_keys_glob_deep_nested() {
966966
type Answer = Equal<'a.b.*' extends Keys ? true : false, true>;
967967
ok(typia.random<Answer>());
968968
}
969+
970+
/**
971+
* Tests that DeepStrictObjectKeys returns never for an empty tuple.
972+
*/
973+
export function test_types_deep_strict_object_keys_empty_tuple() {
974+
type Question = DeepStrictObjectKeys<[]>;
975+
type Answer = Equal<Question, never>;
976+
ok(typia.random<Answer>());
977+
}
978+
979+
/**
980+
* Tests that DeepStrictObjectKeys returns '[*]' for a single-element primitive tuple.
981+
*/
982+
export function test_types_deep_strict_object_keys_primitive_tuple() {
983+
type Question = DeepStrictObjectKeys<['a']>;
984+
type Answer = Equal<Question, '[*]'>;
985+
ok(typia.random<Answer>());
986+
}
987+
988+
/**
989+
* Tests that DeepStrictObjectKeys returns '[*]' for a multi-element primitive tuple.
990+
*/
991+
export function test_types_deep_strict_object_keys_multi_primitive_tuple() {
992+
type Question = DeepStrictObjectKeys<[string, number]>;
993+
type Answer = Equal<Question, '[*]'>;
994+
ok(typia.random<Answer>());
995+
}
996+
997+
/**
998+
* Tests that DeepStrictObjectKeys returns '[*]' for a readonly primitive tuple.
999+
*/
1000+
export function test_types_deep_strict_object_keys_readonly_primitive_tuple() {
1001+
type Question = DeepStrictObjectKeys<readonly ['a']>;
1002+
type Answer = Equal<Question, '[*]'>;
1003+
ok(typia.random<Answer>());
1004+
}
1005+
1006+
/**
1007+
* Tests that DeepStrictObjectKeys returns correct keys for a tuple of objects
1008+
* with the same shape.
1009+
*/
1010+
export function test_types_deep_strict_object_keys_same_shape_object_tuple() {
1011+
type Question = DeepStrictObjectKeys<[{ a: 1 }, { a: 2 }]>;
1012+
type Answer = Equal<Question, '[*]' | '[*].a' | '[*].*'>;
1013+
ok(typia.random<Answer>());
1014+
}
1015+
1016+
/**
1017+
* Tests that DeepStrictObjectKeys returns correct keys for a heterogeneous tuple
1018+
* where elements have different shapes.
1019+
*/
1020+
export function test_types_deep_strict_object_keys_heterogeneous_object_tuple() {
1021+
type Question = DeepStrictObjectKeys<[{ a: 1 }, { b: 2 }]>;
1022+
type Answer = Equal<Question, '[*]' | '[*].a' | '[*].b' | '[*].*'>;
1023+
ok(typia.random<Answer>());
1024+
}
1025+
1026+
/**
1027+
* Tests that DeepStrictObjectKeys correctly recurses into nested tuple elements
1028+
* when the tuple is a property of an object.
1029+
*/
1030+
export function test_types_deep_strict_object_keys_nested_heterogeneous_tuple() {
1031+
type Question = DeepStrictObjectKeys<{ items: [{ a: 1 }, { b: 2 }] }>;
1032+
type Answer = Equal<Question, '*' | 'items' | 'items[*].a' | 'items[*].b' | 'items[*].*'>;
1033+
ok(typia.random<Answer>());
1034+
}
1035+
1036+
/**
1037+
* Tests that DeepStrictObjectKeys handles a nested same-shape object tuple.
1038+
*/
1039+
export function test_types_deep_strict_object_keys_nested_same_shape_tuple() {
1040+
type Question = DeepStrictObjectKeys<{ items: [{ a: 1 }, { a: 2 }] }>;
1041+
type Answer = Equal<Question, '*' | 'items' | 'items[*].a' | 'items[*].*'>;
1042+
ok(typia.random<Answer>());
1043+
}
1044+
1045+
/**
1046+
* Tests that DeepStrictObjectKeys handles a nested primitive tuple
1047+
* (no deeper recursion needed).
1048+
*/
1049+
export function test_types_deep_strict_object_keys_nested_primitive_tuple() {
1050+
type Question = DeepStrictObjectKeys<{ items: [string, number] }>;
1051+
type Answer = Equal<Question, '*' | 'items'>;
1052+
ok(typia.random<Answer>());
1053+
}

0 commit comments

Comments
 (0)