Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
14 changes: 12 additions & 2 deletions src/types/DeepStrictObjectKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, Joiner extends { array: string; object: string }, IsSafe extends boolean> = T extends object
? Infer<T, Joiner, IsSafe>
: never;

/**
* Internal helper type that recursively extracts all keys from nested objects
* @template Target - The object type to extract keys from
Expand Down Expand Up @@ -34,7 +44,7 @@ namespace DeepStrictObjectKeys {
? // For arrays of objects, add array notation and recurse into elements
| P
// | (Equal<IsSafe, true> 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<E, Joiner, IsSafe>}` // recursive
: never // Remove all primitive types of union types.
Expand All @@ -43,7 +53,7 @@ namespace DeepStrictObjectKeys {
? // Handle arrays containing objects
| P
// | (Equal<IsSafe, true> extends true ? never : `${P}[*]`) // end of array
| `${P}${Joiner['array']}${Joiner['object']}${Infer<Element, Joiner, false>}`
| `${P}${Joiner['array']}${Joiner['object']}${DistributeInfer<Element, Joiner, false>}`
: Target[P] extends Array<infer _Element>
? // Handle arrays containing primitives
Equal<IsSafe, true> extends true
Expand Down
85 changes: 85 additions & 0 deletions test/features/DeepStrictObjectKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Answer>());
}

/**
* 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<Question, never>;
ok(typia.random<Answer>());
}

/**
* 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<Question, '[*]'>;
ok(typia.random<Answer>());
}

/**
* 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<Question, '[*]'>;
ok(typia.random<Answer>());
}

/**
* Tests that DeepStrictObjectKeys returns '[*]' for a readonly primitive tuple.
*/
export function test_types_deep_strict_object_keys_readonly_primitive_tuple() {
type Question = DeepStrictObjectKeys<readonly ['a']>;
type Answer = Equal<Question, '[*]'>;
ok(typia.random<Answer>());
}

/**
* 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<Question, '[*]' | '[*].a' | '[*].*'>;
ok(typia.random<Answer>());
}

/**
* 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<Question, '[*]' | '[*].a' | '[*].b' | '[*].*'>;
ok(typia.random<Answer>());
}

/**
* 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<Question, '*' | 'items' | 'items[*].a' | 'items[*].b' | 'items[*].*'>;
ok(typia.random<Answer>());
}

/**
* 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<Question, '*' | 'items' | 'items[*].a' | 'items[*].*'>;
ok(typia.random<Answer>());
}

/**
* 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<Question, '*' | 'items'>;
ok(typia.random<Answer>());
}