Skip to content

Commit b641ada

Browse files
authored
Merge pull request #27 from kakasoo/kakasoo/fix-deep-strict-pick
feat: add glob (*) pattern to DeepStrictObjectKeys
2 parents 03be5c5 + 34f0580 commit b641ada

20 files changed

+598
-205
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: 🧪 Build · Test
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'src/**'
7+
- 'test/**'
8+
- 'package.json'
9+
- '.github/workflows/build-and-test.yml'
10+
11+
jobs:
12+
build-and-test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: 20.x
20+
21+
- name: Install dependencies
22+
run: npm install
23+
24+
- name: Build and run tests
25+
run: npm run build:test && npm run test

CLAUDE.md

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -106,28 +106,43 @@ export function test_types_deep_strict_pick_nested() {
106106
}
107107
```
108108

109+
## PR 체크리스트
110+
111+
PR을 생성하기 전에 **반드시** 다음을 수행합니다:
112+
113+
1. `npm run build:test && npm run test` — 모든 테스트 통과 확인
114+
2. **`npm run prettier`** — 코드 포매팅 (필수, 빠뜨리지 말 것)
115+
3. 커밋 및 푸시
116+
109117
## Git Conventions
110118

111119
### 브랜치 규칙
112120

113121
- 기본 브랜치: `main`
114-
- 기능 브랜치: `feature/<name>`, `fix/<name>`, `docs/<name>`
122+
- 기능 브랜치: `kakasoo/<name>` (예: `kakasoo/deep-strict-flat`, `kakasoo/fix-deep-strict-pick`)
115123

116124
### 커밋 메시지
117125

118126
영어로 작성하며 다음 prefix를 사용합니다:
119127

120-
| Prefix | 용도 |
121-
|--------|------|
122-
| `feat` | 새로운 타입/함수 추가 |
123-
| `fix` | 타입 버그 수정 |
124-
| `test` | 테스트 추가/수정 |
125-
| `docs` | 문서/주석 변경 |
126-
| `refactor` | 리팩토링 |
127-
| `style` | 코드 포맷팅 |
128-
| `chore` | 빌드, 설정 변경 |
129-
130-
형식: `<prefix>: <영어 설명>`
128+
| Prefix | 용도 | 예시 |
129+
|--------|------|------|
130+
| `feat` | 새로운 타입/함수 추가 | `feat: add glob (*) pattern to DeepStrictObjectKeys for wildcard key selection` |
131+
| `fix` | 타입 버그 수정 | `fix: support readonly array type` |
132+
| `test` | 테스트 추가/수정 | `test: add comprehensive test coverage for all types and functions` |
133+
| `docs` | 문서/주석 변경 | `docs: rewrite README with installation, quick start, and full API coverage` |
134+
| `refactor` | 리팩토링 | `refactor: extract to isMatched function` |
135+
| `style` | 코드 포맷팅 | `style: remove unnecessary type` |
136+
| `chore` | 빌드, 설정 변경 | `chore: update test code command` |
137+
| `ci` | CI/CD 변경 | `ci: add test step before npm publish` |
138+
139+
형식: `<prefix>: <소문자로 시작하는 영어 설명>`
140+
141+
규칙:
142+
- prefix 뒤에 콜론과 공백 (`: `)
143+
- 설명은 소문자로 시작, 마침표 없이 끝냄
144+
- 코드 참조 시 백틱 사용 가능 (예: `` chore: add detailed comments for `DeepStrictObjectKeys` ``)
145+
- 한 줄로 작성, 무엇을 했는지 간결하게 기술
131146

132147
## CI/CD
133148

src/functions/DeepStrictObjectKeys.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@ type Replace<S extends string> = S extends '[*]'
1919
* Converts type-level keys (with `[*]` notation) to runtime-friendly keys (with `${number}` notation),
2020
* strips leading dots, and wraps the result in an array type.
2121
*/
22+
/** @internal Removes glob patterns (e.g., `'*'`, `'a.*'`) from a string union. */
23+
type WithoutGlob<K extends string> = K extends '*' | `${string}.*` ? never : K;
24+
2225
type ReturnType<
2326
Target extends object,
2427
Joiner extends { array: string; object: string } = { array: '[*]'; object: '.' },
25-
> = [Target] extends [never] ? [] : RemoveStartWithDot<Replace<DeepStrictObjectKeys<Target, Joiner, false>>>[];
28+
> = [Target] extends [never]
29+
? []
30+
: RemoveStartWithDot<Replace<WithoutGlob<DeepStrictObjectKeys<Target, Joiner, false>>>>[];
2631

2732
/**
2833
* @title Runtime Function for Extracting All Nested Keys from an Object.

src/types/DeepStrictObjectKeys.ts

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,45 +19,47 @@ namespace DeepStrictObjectKeys {
1919
P extends keyof Target = Exclude<keyof Target, keyof []>,
2020
> = [Target] extends [never]
2121
? never
22-
: P extends string
23-
? IsUnion<Target[P]> extends true
24-
? Equal<IsSafe, true> extends true
25-
? P // In safe mode, only return the key itself for union types
26-
: // In unsafe mode, explore union types that mix primitives and objects
27-
| P
28-
| (Target[P] extends infer E
29-
? E extends ValueType
30-
? P // For primitive types, just return the key
31-
: E extends object
32-
? E extends Array<infer _Element extends object>
33-
? // For arrays of objects, add array notation and recurse into elements
34-
| P
35-
// | (Equal<IsSafe, true> extends true ? never : `${P}[*]`) // end of array
36-
| `${P}${Joiner['array']}${Joiner['object']}${Infer<_Element, Joiner, IsSafe>}` // recursive
37-
: // For regular objects, add object notation and recurse
38-
`${P}${Joiner['object']}${Infer<E, Joiner, IsSafe>}` // recursive
39-
: never // Remove all primitive types of union types.
40-
: never)
41-
: Target[P] extends Array<infer Element extends object>
42-
? // Handle arrays containing objects
43-
| P
44-
// | (Equal<IsSafe, true> extends true ? never : `${P}[*]`) // end of array
45-
| `${P}${Joiner['array']}${Joiner['object']}${Infer<Element, Joiner, false>}`
46-
: Target[P] extends Array<infer _Element>
47-
? // Handle arrays containing primitives
48-
Equal<IsSafe, true> extends true
49-
? P
50-
: P | never // `${P}[*]`
51-
: Target[P] extends ValueType
52-
? P // For primitive values, just return the key
53-
: IsAny<Target[P]> extends true
54-
? P // For 'any' type, return the key
55-
: Target[P] extends object
56-
? Target[P] extends Record<string, never>
57-
? `${P}` // For empty objects, just return the key
58-
: `${P}` | `${P}${Joiner['object']}${Infer<Target[P], Joiner, false>}` // For objects with properties, include both the key and nested paths
59-
: never
60-
: never;
22+
:
23+
| (P extends string
24+
? IsUnion<Target[P]> extends true
25+
? Equal<IsSafe, true> extends true
26+
? P // In safe mode, only return the key itself for union types
27+
: // In unsafe mode, explore union types that mix primitives and objects
28+
| P
29+
| (Target[P] extends infer E
30+
? E extends ValueType
31+
? P // For primitive types, just return the key
32+
: E extends object
33+
? E extends Array<infer _Element extends object>
34+
? // For arrays of objects, add array notation and recurse into elements
35+
| P
36+
// | (Equal<IsSafe, true> extends true ? never : `${P}[*]`) // end of array
37+
| `${P}${Joiner['array']}${Joiner['object']}${Infer<_Element, Joiner, IsSafe>}` // recursive
38+
: // For regular objects, add object notation and recurse
39+
`${P}${Joiner['object']}${Infer<E, Joiner, IsSafe>}` // recursive
40+
: never // Remove all primitive types of union types.
41+
: never)
42+
: Target[P] extends Array<infer Element extends object>
43+
? // Handle arrays containing objects
44+
| P
45+
// | (Equal<IsSafe, true> extends true ? never : `${P}[*]`) // end of array
46+
| `${P}${Joiner['array']}${Joiner['object']}${Infer<Element, Joiner, false>}`
47+
: Target[P] extends Array<infer _Element>
48+
? // Handle arrays containing primitives
49+
Equal<IsSafe, true> extends true
50+
? P
51+
: P | never // `${P}[*]`
52+
: Target[P] extends ValueType
53+
? P // For primitive values, just return the key
54+
: IsAny<Target[P]> extends true
55+
? P // For 'any' type, return the key
56+
: Target[P] extends object
57+
? Target[P] extends Record<string, never>
58+
? `${P}` // For empty objects, just return the key
59+
: `${P}` | `${P}${Joiner['object']}${Infer<Target[P], Joiner, false>}` // For objects with properties, include both the key and nested paths
60+
: never
61+
: never)
62+
| ([Exclude<keyof Target, keyof []>] extends [never] ? never : '*');
6163
}
6264

6365
/**

src/types/DeepStrictOmit.ts

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,31 @@ namespace DeepStrictOmit {
1515
* @template T - The object type to omit keys from
1616
* @template K - The dot-notation key paths to omit (must be valid keys of `T`)
1717
*/
18-
export type Infer<T extends object, K extends DeepStrictObjectKeys<T>> = [K] extends [never]
19-
? T
20-
: {
21-
[key in keyof T as key extends K ? never : key]: T[key] extends Array<infer Element extends object>
22-
? key extends string
23-
? Element extends Date
18+
export type Infer<T extends object, K extends DeepStrictObjectKeys<T>> = '*' extends K
19+
? {}
20+
: [K] extends [never]
21+
? T
22+
: {
23+
[key in keyof T as key extends K ? never : key]: T[key] extends Array<infer Element extends object>
24+
? key extends string
25+
? Element extends Date
26+
? Array<Element>
27+
: GetElementMember<K, key> extends DeepStrictObjectKeys<Element>
28+
? Array<Infer<Element, GetElementMember<K, key>>>
29+
: Array<Element>
30+
: never
31+
: T[key] extends Array<infer Element>
2432
? Array<Element>
25-
: GetElementMember<K, key> extends DeepStrictObjectKeys<Element>
26-
? Array<Infer<Element, GetElementMember<K, key>>>
27-
: Array<Element>
28-
: never
29-
: T[key] extends Array<infer Element>
30-
? Array<Element>
31-
: T[key] extends object
32-
? key extends string
33-
? T[key] extends Date
34-
? T[key]
35-
: GetElementMember<K, key> extends DeepStrictObjectKeys<T[key]>
36-
? Infer<T[key], GetElementMember<K, key>>
37-
: T[key]
38-
: never
39-
: T[key];
40-
};
33+
: T[key] extends object
34+
? key extends string
35+
? T[key] extends Date
36+
? T[key]
37+
: GetElementMember<K, key> extends DeepStrictObjectKeys<T[key]>
38+
? Infer<T[key], GetElementMember<K, key>>
39+
: T[key]
40+
: never
41+
: T[key];
42+
};
4143
}
4244

4345
/**
@@ -57,8 +59,11 @@ namespace DeepStrictOmit {
5759
* type Example3 = DeepStrictOmit<{ a: 1 }[], "[*].a">; // {}[]
5860
* ```
5961
*/
60-
export type DeepStrictOmit<T extends object, K extends DeepStrictObjectKeys<T>> =
61-
T extends Array<infer Element extends object>
62+
export type DeepStrictOmit<T extends object, K extends DeepStrictObjectKeys<T>> = '*' extends K
63+
? T extends Array<any>
64+
? never[]
65+
: {}
66+
: T extends Array<infer Element extends object>
6267
? Array<
6368
DeepStrictOmit<
6469
Element,

src/types/DeepStrictPick.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { DeepStrictObjectKeys } from './DeepStrictObjectKeys';
22
import type { DeepStrictOmit } from './DeepStrictOmit';
33
import type { DeepStrictUnbrand } from './DeepStrictUnbrand';
4+
import type { ExpandGlob } from './ExpandGlob';
45
import type { RemoveAfterDot } from './RemoveAfterDot';
56
import type { RemoveLastProperty } from './RemoveLastProperty';
67

@@ -23,10 +24,15 @@ import type { RemoveLastProperty } from './RemoveLastProperty';
2324
* type Example3 = DeepStrictPick<{ a: 1 }[], "[*].a">; // { a: 1 }[]
2425
* ```
2526
*/
26-
export type DeepStrictPick<T extends object, K extends DeepStrictObjectKeys<T>> = DeepStrictOmit<
27-
T,
28-
Exclude<
29-
DeepStrictObjectKeys<T>, //
30-
K | RemoveLastProperty<K> | RemoveAfterDot<DeepStrictUnbrand<T>, K>
31-
>
32-
>;
27+
export type DeepStrictPick<T extends object, K extends DeepStrictObjectKeys<T>> = '*' extends K
28+
? T
29+
: DeepStrictOmit<
30+
T,
31+
Exclude<
32+
Exclude<
33+
DeepStrictObjectKeys<T>,
34+
K | RemoveLastProperty<K> | RemoveAfterDot<DeepStrictUnbrand<T>, K> | ExpandGlob<K>
35+
>,
36+
'*' | `${string}.*`
37+
>
38+
>;

src/types/Equal.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Helper type that creates a generic function signature for type comparison.
33
* This technique leverages TypeScript's strict function type checking to determine type equality.
4-
*
4+
*
55
* @internal
66
*/
77
type Expression<X> = <T>() => T extends X ? 1 : 2;
@@ -12,7 +12,7 @@ type Expression<X> = <T>() => T extends X ? 1 : 2;
1212
* The `Equal<X, Y>` type uses conditional types and a helper type `Expression<X>`
1313
* to determine if two types `X` and `Y` are exactly the same. It returns `true` if they are
1414
* equal, and `false` otherwise.
15-
*
15+
*
1616
* This type performs a strict equality check that distinguishes between:
1717
* - Union types with different members
1818
* - Branded types vs their base types

src/types/ExpandGlob.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* @title Type for Expanding Glob Patterns into Template Literal Matchers.
3+
*
4+
* Converts glob patterns (`*`) into template literal types that can match
5+
* all keys at a given level. Used by {@link DeepStrictPick} to correctly
6+
* preserve all descendant keys when a glob pattern is selected.
7+
*
8+
* - `'*'` expands to `string` (matches everything)
9+
* - `'a.*'` expands to `'a.${string}' | 'a'` (matches all keys under `a`)
10+
* - `'items[*].*'` expands to `'items[*].${string}' | 'items[*]'`
11+
*
12+
* @template K - The key pattern to expand
13+
* @returns A template literal type matching all keys covered by the glob, or `never` for non-glob keys
14+
*
15+
* @example
16+
* ```typescript
17+
* type Ex1 = ExpandGlob<'*'>; // string
18+
* type Ex2 = ExpandGlob<'a.*'>; // `a.${string}` | 'a'
19+
* type Ex3 = ExpandGlob<'items[*].*'>; // `items[*].${string}` | 'items[*]'
20+
* type Ex4 = ExpandGlob<'a.b'>; // never (not a glob pattern)
21+
* ```
22+
*/
23+
export type ExpandGlob<K extends string> = K extends '*'
24+
? string
25+
: K extends `${infer Prefix}.*`
26+
? `${Prefix}.${string}` | Prefix
27+
: never;

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './DeepStrictPick';
77
export * from './DeepStrictUnbrand';
88
export * from './ElementOf';
99
export * from './Equal';
10+
export * from './ExpandGlob';
1011
export * from './GetMember';
1112
export * from './GetType';
1213
export * from './IsAny';

test/features/DeepStrictMerge.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,7 @@ export function test_types_deep_strict_merge_nested_objects() {
3333
* Tests that DeepStrictMerge recursively merges nested objects with overlapping keys (Target wins).
3434
*/
3535
export function test_types_deep_strict_merge_nested_overlapping() {
36-
type Question = DeepStrictMerge<
37-
{ a: { b: number; c: string } },
38-
{ a: { b: string; d: boolean } }
39-
>;
36+
type Question = DeepStrictMerge<{ a: { b: number; c: string } }, { a: { b: string; d: boolean } }>;
4037
type Answer = Equal<Question, { a: { b: number; c: string; d: boolean } }>;
4138
ok(typia.random<Answer>());
4239
}
@@ -54,10 +51,7 @@ export function test_types_deep_strict_merge_array_top_level() {
5451
* Tests that DeepStrictMerge merges array-typed properties within objects.
5552
*/
5653
export function test_types_deep_strict_merge_array_property() {
57-
type Question = DeepStrictMerge<
58-
{ items: { a: number }[] },
59-
{ items: { b: string }[] }
60-
>;
54+
type Question = DeepStrictMerge<{ items: { a: number }[] }, { items: { b: string }[] }>;
6155
type Answer = Equal<Question, { items: { a: number; b: string }[] }>;
6256
ok(typia.random<Answer>());
6357
}
@@ -75,10 +69,7 @@ export function test_types_deep_strict_merge_array_vs_non_array() {
7569
* Tests that DeepStrictMerge returns never for property-level array vs non-array mismatch.
7670
*/
7771
export function test_types_deep_strict_merge_property_array_mismatch() {
78-
type Question = DeepStrictMerge<
79-
{ items: { a: number }[] },
80-
{ items: { b: string } }
81-
>;
72+
type Question = DeepStrictMerge<{ items: { a: number }[] }, { items: { b: string } }>;
8273
type Answer = Equal<Question, { items: never }>;
8374
ok(typia.random<Answer>());
8475
}
@@ -87,10 +78,7 @@ export function test_types_deep_strict_merge_property_array_mismatch() {
8778
* Tests that DeepStrictMerge handles deeply nested structures (3+ levels).
8879
*/
8980
export function test_types_deep_strict_merge_deeply_nested() {
90-
type Question = DeepStrictMerge<
91-
{ a: { b: { c: number } } },
92-
{ a: { b: { d: string } } }
93-
>;
81+
type Question = DeepStrictMerge<{ a: { b: { c: number } } }, { a: { b: { d: string } } }>;
9482
type Answer = Equal<Question, { a: { b: { c: number; d: string } } }>;
9583
ok(typia.random<Answer>());
9684
}
@@ -99,10 +87,7 @@ export function test_types_deep_strict_merge_deeply_nested() {
9987
* Tests that DeepStrictMerge preserves Source-only keys at nested levels.
10088
*/
10189
export function test_types_deep_strict_merge_source_only_nested() {
102-
type Question = DeepStrictMerge<
103-
{ a: { b: number } },
104-
{ a: { c: string }; d: boolean }
105-
>;
90+
type Question = DeepStrictMerge<{ a: { b: number } }, { a: { c: string }; d: boolean }>;
10691
type Answer = Equal<Question, { a: { b: number; c: string }; d: boolean }>;
10792
ok(typia.random<Answer>());
10893
}

0 commit comments

Comments
 (0)