From 83578ce519940689f915267ab58dcd5bf90dafcf Mon Sep 17 00:00:00 2001 From: kakasoo Date: Thu, 19 Feb 2026 23:42:16 +0900 Subject: [PATCH] fix: support tuple types in `DeepStrictObjectKeys` by distributing over union element types Heterogeneous tuples (e.g., [{ a: 1 }, { b: 2 }]) were losing nested keys because Infer did not distribute over union element types, causing keyof (A | B) to resolve to never. Added DistributeInfer wrapper and comprehensive tuple tests. Co-Authored-By: Claude Opus 4.6 --- src/types/DeepStrictObjectKeys.ts | 14 ++++- test/features/DeepStrictObjectKeys.ts | 85 +++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/src/types/DeepStrictObjectKeys.ts b/src/types/DeepStrictObjectKeys.ts index fcdc28c..ffba3e5 100644 --- a/src/types/DeepStrictObjectKeys.ts +++ b/src/types/DeepStrictObjectKeys.ts @@ -5,6 +5,16 @@ import type { IsUnion } from './IsUnion'; import type { ValueType } from './ValueType'; namespace DeepStrictObjectKeys { + /** + * Distributive wrapper for Infer that properly handles union element types. + * When Element is a union (e.g., {a: 1} | {b: 2} from tuple [{ a: 1 }, { b: 2 }]), + * this distributes the Infer call to each member of the union individually, + * avoiding the issue where keyof (A | B) = (keyof A) & (keyof B) = never. + */ + type DistributeInfer = T extends object + ? Infer + : never; + /** * Internal helper type that recursively extracts all keys from nested objects * @template Target - The object type to extract keys from @@ -34,7 +44,7 @@ namespace DeepStrictObjectKeys { ? // For arrays of objects, add array notation and recurse into elements | P // | (Equal extends true ? never : `${P}[*]`) // end of array - | `${P}${Joiner['array']}${Joiner['object']}${Infer<_Element, Joiner, IsSafe>}` // recursive + | `${P}${Joiner['array']}${Joiner['object']}${DistributeInfer<_Element, Joiner, IsSafe>}` // recursive : // For regular objects, add object notation and recurse `${P}${Joiner['object']}${Infer}` // recursive : never // Remove all primitive types of union types. @@ -43,7 +53,7 @@ namespace DeepStrictObjectKeys { ? // Handle arrays containing objects | P // | (Equal extends true ? never : `${P}[*]`) // end of array - | `${P}${Joiner['array']}${Joiner['object']}${Infer}` + | `${P}${Joiner['array']}${Joiner['object']}${DistributeInfer}` : Target[P] extends Array ? // Handle arrays containing primitives Equal extends true diff --git a/test/features/DeepStrictObjectKeys.ts b/test/features/DeepStrictObjectKeys.ts index deb6e9a..239f9f3 100644 --- a/test/features/DeepStrictObjectKeys.ts +++ b/test/features/DeepStrictObjectKeys.ts @@ -966,3 +966,88 @@ export function test_types_deep_strict_object_keys_glob_deep_nested() { type Answer = Equal<'a.b.*' extends Keys ? true : false, true>; ok(typia.random()); } + +/** + * Tests that DeepStrictObjectKeys returns never for an empty tuple. + */ +export function test_types_deep_strict_object_keys_empty_tuple() { + type Question = DeepStrictObjectKeys<[]>; + type Answer = Equal; + ok(typia.random()); +} + +/** + * Tests that DeepStrictObjectKeys returns '[*]' for a single-element primitive tuple. + */ +export function test_types_deep_strict_object_keys_primitive_tuple() { + type Question = DeepStrictObjectKeys<['a']>; + type Answer = Equal; + ok(typia.random()); +} + +/** + * Tests that DeepStrictObjectKeys returns '[*]' for a multi-element primitive tuple. + */ +export function test_types_deep_strict_object_keys_multi_primitive_tuple() { + type Question = DeepStrictObjectKeys<[string, number]>; + type Answer = Equal; + ok(typia.random()); +} + +/** + * Tests that DeepStrictObjectKeys returns '[*]' for a readonly primitive tuple. + */ +export function test_types_deep_strict_object_keys_readonly_primitive_tuple() { + type Question = DeepStrictObjectKeys; + type Answer = Equal; + ok(typia.random()); +} + +/** + * Tests that DeepStrictObjectKeys returns correct keys for a tuple of objects + * with the same shape. + */ +export function test_types_deep_strict_object_keys_same_shape_object_tuple() { + type Question = DeepStrictObjectKeys<[{ a: 1 }, { a: 2 }]>; + type Answer = Equal; + ok(typia.random()); +} + +/** + * Tests that DeepStrictObjectKeys returns correct keys for a heterogeneous tuple + * where elements have different shapes. + */ +export function test_types_deep_strict_object_keys_heterogeneous_object_tuple() { + type Question = DeepStrictObjectKeys<[{ a: 1 }, { b: 2 }]>; + type Answer = Equal; + ok(typia.random()); +} + +/** + * Tests that DeepStrictObjectKeys correctly recurses into nested tuple elements + * when the tuple is a property of an object. + */ +export function test_types_deep_strict_object_keys_nested_heterogeneous_tuple() { + type Question = DeepStrictObjectKeys<{ items: [{ a: 1 }, { b: 2 }] }>; + type Answer = Equal; + ok(typia.random()); +} + +/** + * Tests that DeepStrictObjectKeys handles a nested same-shape object tuple. + */ +export function test_types_deep_strict_object_keys_nested_same_shape_tuple() { + type Question = DeepStrictObjectKeys<{ items: [{ a: 1 }, { a: 2 }] }>; + type Answer = Equal; + ok(typia.random()); +} + +/** + * Tests that DeepStrictObjectKeys handles a nested primitive tuple + * (no deeper recursion needed). + */ +export function test_types_deep_strict_object_keys_nested_primitive_tuple() { + type Question = DeepStrictObjectKeys<{ items: [string, number] }>; + type Answer = Equal; + ok(typia.random()); +}