@simplysm/core-common 13.0.0-beta.2 → 13.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common.types.js +4 -4
- package/dist/errors/argument-error.js +1 -1
- package/dist/errors/not-implemented-error.js +1 -1
- package/dist/errors/timeout-error.js +1 -1
- package/dist/extensions/arr-ext.helpers.js +4 -4
- package/dist/extensions/arr-ext.js +9 -9
- package/dist/features/debounce-queue.js +2 -2
- package/dist/features/serial-queue.js +3 -3
- package/dist/index.js +30 -30
- package/dist/types/date-only.js +2 -2
- package/dist/types/date-time.js +2 -2
- package/dist/types/time.js +2 -2
- package/dist/types/uuid.js +1 -1
- package/dist/utils/bytes.js +1 -1
- package/dist/utils/json.js +8 -8
- package/dist/utils/obj.js +5 -5
- package/dist/utils/primitive.js +5 -5
- package/dist/utils/transferable.js +4 -4
- package/dist/utils/wait.js +1 -1
- package/package.json +7 -4
- package/.cache/typecheck-browser.tsbuildinfo +0 -1
- package/.cache/typecheck-node.tsbuildinfo +0 -1
- package/.cache/typecheck-tests-browser.tsbuildinfo +0 -1
- package/.cache/typecheck-tests-node.tsbuildinfo +0 -1
- package/src/common.types.ts +0 -91
- package/src/env.ts +0 -11
- package/src/errors/argument-error.ts +0 -40
- package/src/errors/not-implemented-error.ts +0 -32
- package/src/errors/sd-error.ts +0 -53
- package/src/errors/timeout-error.ts +0 -36
- package/src/extensions/arr-ext.helpers.ts +0 -53
- package/src/extensions/arr-ext.ts +0 -777
- package/src/extensions/arr-ext.types.ts +0 -258
- package/src/extensions/map-ext.ts +0 -86
- package/src/extensions/set-ext.ts +0 -68
- package/src/features/debounce-queue.ts +0 -116
- package/src/features/event-emitter.ts +0 -112
- package/src/features/serial-queue.ts +0 -94
- package/src/globals.ts +0 -12
- package/src/index.ts +0 -55
- package/src/types/date-only.ts +0 -329
- package/src/types/date-time.ts +0 -294
- package/src/types/lazy-gc-map.ts +0 -244
- package/src/types/time.ts +0 -210
- package/src/types/uuid.ts +0 -113
- package/src/utils/bytes.ts +0 -160
- package/src/utils/date-format.ts +0 -239
- package/src/utils/json.ts +0 -230
- package/src/utils/num.ts +0 -97
- package/src/utils/obj.ts +0 -956
- package/src/utils/path.ts +0 -40
- package/src/utils/primitive.ts +0 -33
- package/src/utils/str.ts +0 -252
- package/src/utils/template-strings.ts +0 -132
- package/src/utils/transferable.ts +0 -269
- package/src/utils/wait.ts +0 -40
- package/src/utils/xml.ts +0 -105
- package/src/zip/sd-zip.ts +0 -218
- package/tests/errors/errors.spec.ts +0 -196
- package/tests/extensions/array-extension.spec.ts +0 -790
- package/tests/extensions/map-extension.spec.ts +0 -147
- package/tests/extensions/set-extension.spec.ts +0 -74
- package/tests/types/date-only.spec.ts +0 -636
- package/tests/types/date-time.spec.ts +0 -391
- package/tests/types/lazy-gc-map.spec.ts +0 -692
- package/tests/types/time.spec.ts +0 -559
- package/tests/types/types.spec.ts +0 -55
- package/tests/types/uuid.spec.ts +0 -91
- package/tests/utils/bytes-utils.spec.ts +0 -230
- package/tests/utils/date-format.spec.ts +0 -371
- package/tests/utils/debounce-queue.spec.ts +0 -272
- package/tests/utils/json.spec.ts +0 -475
- package/tests/utils/number.spec.ts +0 -184
- package/tests/utils/object.spec.ts +0 -827
- package/tests/utils/path.spec.ts +0 -78
- package/tests/utils/primitive.spec.ts +0 -55
- package/tests/utils/sd-event-emitter.spec.ts +0 -216
- package/tests/utils/serial-queue.spec.ts +0 -365
- package/tests/utils/string.spec.ts +0 -294
- package/tests/utils/template-strings.spec.ts +0 -96
- package/tests/utils/transferable.spec.ts +0 -698
- package/tests/utils/wait.spec.ts +0 -145
- package/tests/utils/xml.spec.ts +0 -146
- package/tests/zip/sd-zip.spec.ts +0 -234
package/src/utils/obj.ts
DELETED
|
@@ -1,956 +0,0 @@
|
|
|
1
|
-
import { DateTime } from "../types/date-time";
|
|
2
|
-
import { DateOnly } from "../types/date-only";
|
|
3
|
-
import { Time } from "../types/time";
|
|
4
|
-
import { Uuid } from "../types/uuid";
|
|
5
|
-
import { ArgumentError } from "../errors/argument-error";
|
|
6
|
-
|
|
7
|
-
//#region objClone
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* 깊은 복사
|
|
11
|
-
* - 순환 참조 지원
|
|
12
|
-
* - 커스텀 타입(DateTime, DateOnly, Time, Uuid, Uint8Array) 복사 지원
|
|
13
|
-
*
|
|
14
|
-
* @note 함수, Symbol은 복사되지 않고 참조가 유지됨
|
|
15
|
-
* @note WeakMap, WeakSet은 지원되지 않음 (일반 객체로 복사되어 빈 객체가 됨)
|
|
16
|
-
* @note 프로토타입 체인은 유지됨 (Object.setPrototypeOf 사용)
|
|
17
|
-
* @note getter/setter는 현재 값으로 평가되어 복사됨 (접근자 속성 자체는 복사되지 않음)
|
|
18
|
-
*/
|
|
19
|
-
export function objClone<T>(source: T): T {
|
|
20
|
-
return objCloneImpl(source) as T;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function objCloneImpl(source: unknown, prevClones?: WeakMap<object, unknown>): unknown {
|
|
24
|
-
// primitive는 그대로 반환
|
|
25
|
-
if (typeof source !== "object" || source === null) {
|
|
26
|
-
return source;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Immutable-like 타입들 (내부에 object 참조 없음)
|
|
30
|
-
if (source instanceof Date) {
|
|
31
|
-
return new Date(source.getTime());
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (source instanceof DateTime) {
|
|
35
|
-
return new DateTime(source.tick);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (source instanceof DateOnly) {
|
|
39
|
-
return new DateOnly(source.tick);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (source instanceof Time) {
|
|
43
|
-
return new Time(source.tick);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (source instanceof Uuid) {
|
|
47
|
-
return new Uuid(source.toString());
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// RegExp
|
|
51
|
-
if (source instanceof RegExp) {
|
|
52
|
-
return new RegExp(source.source, source.flags);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// 순환 참조 체크 (Error 포함 모든 object 타입에 적용)
|
|
56
|
-
const currPrevClones = prevClones ?? new WeakMap<object, unknown>();
|
|
57
|
-
if (currPrevClones.has(source)) {
|
|
58
|
-
return currPrevClones.get(source);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Error (cause 포함)
|
|
62
|
-
// 생성자 호출 대신 프로토타입 기반 복사 - 커스텀 Error 클래스 호환성 보장
|
|
63
|
-
if (source instanceof Error) {
|
|
64
|
-
const cloned = Object.create(Object.getPrototypeOf(source)) as Error;
|
|
65
|
-
currPrevClones.set(source, cloned);
|
|
66
|
-
cloned.message = source.message;
|
|
67
|
-
cloned.name = source.name;
|
|
68
|
-
cloned.stack = source.stack;
|
|
69
|
-
if (source.cause !== undefined) {
|
|
70
|
-
cloned.cause = objCloneImpl(source.cause, currPrevClones);
|
|
71
|
-
}
|
|
72
|
-
// 커스텀 Error 속성 복사
|
|
73
|
-
for (const key of Object.keys(source)) {
|
|
74
|
-
if (!["message", "name", "stack", "cause"].includes(key)) {
|
|
75
|
-
(cloned as unknown as Record<string, unknown>)[key] = objCloneImpl(
|
|
76
|
-
(source as unknown as Record<string, unknown>)[key],
|
|
77
|
-
currPrevClones,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return cloned;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (source instanceof Uint8Array) {
|
|
85
|
-
const result = source.slice();
|
|
86
|
-
currPrevClones.set(source, result);
|
|
87
|
-
return result;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (source instanceof Array) {
|
|
91
|
-
const result: unknown[] = [];
|
|
92
|
-
currPrevClones.set(source, result);
|
|
93
|
-
for (const item of source) {
|
|
94
|
-
result.push(objCloneImpl(item, currPrevClones));
|
|
95
|
-
}
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (source instanceof Map) {
|
|
100
|
-
const result = new Map();
|
|
101
|
-
currPrevClones.set(source, result);
|
|
102
|
-
for (const [key, value] of source) {
|
|
103
|
-
result.set(objCloneImpl(key, currPrevClones), objCloneImpl(value, currPrevClones));
|
|
104
|
-
}
|
|
105
|
-
return result;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (source instanceof Set) {
|
|
109
|
-
const result = new Set();
|
|
110
|
-
currPrevClones.set(source, result);
|
|
111
|
-
for (const item of source) {
|
|
112
|
-
result.add(objCloneImpl(item, currPrevClones));
|
|
113
|
-
}
|
|
114
|
-
return result;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// 기타 Object
|
|
118
|
-
const result: Record<string, unknown> = {};
|
|
119
|
-
Object.setPrototypeOf(result, Object.getPrototypeOf(source));
|
|
120
|
-
currPrevClones.set(source, result);
|
|
121
|
-
|
|
122
|
-
for (const key of Object.keys(source)) {
|
|
123
|
-
const value = (source as Record<string, unknown>)[key];
|
|
124
|
-
result[key] = objCloneImpl(value, currPrevClones);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return result;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
//#endregion
|
|
131
|
-
|
|
132
|
-
//#region objEqual
|
|
133
|
-
|
|
134
|
-
/** objEqual 옵션 타입 */
|
|
135
|
-
export interface EqualOptions {
|
|
136
|
-
/** 비교할 키 목록. 지정 시 해당 키만 비교 (최상위 레벨에만 적용) */
|
|
137
|
-
topLevelIncludes?: string[];
|
|
138
|
-
/** 비교에서 제외할 키 목록 (최상위 레벨에만 적용) */
|
|
139
|
-
topLevelExcludes?: string[];
|
|
140
|
-
/** 배열 순서 무시 여부. true 시 O(n²) 복잡도 */
|
|
141
|
-
ignoreArrayIndex?: boolean;
|
|
142
|
-
/** 얕은 비교 여부. true 시 1단계만 비교 (참조 비교) */
|
|
143
|
-
onlyOneDepth?: boolean;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* 깊은 비교
|
|
148
|
-
*
|
|
149
|
-
* @param source 비교 대상 1
|
|
150
|
-
* @param target 비교 대상 2
|
|
151
|
-
* @param options 비교 옵션
|
|
152
|
-
* @param options.topLevelIncludes 비교할 키 목록. 지정 시 해당 키만 비교 (최상위 레벨에만 적용)
|
|
153
|
-
* @example `{ topLevelIncludes: ["id", "name"] }` - id, name 키만 비교
|
|
154
|
-
* @param options.topLevelExcludes 비교에서 제외할 키 목록 (최상위 레벨에만 적용)
|
|
155
|
-
* @example `{ topLevelExcludes: ["updatedAt"] }` - updatedAt 키를 제외하고 비교
|
|
156
|
-
* @param options.ignoreArrayIndex 배열 순서 무시 여부. true 시 O(n²) 복잡도
|
|
157
|
-
* @param options.onlyOneDepth 얕은 비교 여부. true 시 1단계만 비교 (참조 비교)
|
|
158
|
-
*
|
|
159
|
-
* @note topLevelIncludes/topLevelExcludes 옵션은 object 속성 키에만 적용됨.
|
|
160
|
-
* Map의 모든 키는 항상 비교에 포함됨.
|
|
161
|
-
* @note 성능 고려사항:
|
|
162
|
-
* - 기본 배열 비교: O(n) 시간 복잡도
|
|
163
|
-
* - `ignoreArrayIndex: true` 사용 시: O(n²) 시간 복잡도
|
|
164
|
-
* (대용량 배열에서 성능 저하 가능)
|
|
165
|
-
* @note `ignoreArrayIndex: true` 동작 특성:
|
|
166
|
-
* - 배열 순서를 무시하고 동일한 요소들의 순열인지 비교
|
|
167
|
-
* - 예: `[1,2,3]`과 `[3,2,1]` → true, `[1,1,1]`과 `[1,2,3]` → false
|
|
168
|
-
*/
|
|
169
|
-
export function objEqual(source: unknown, target: unknown, options?: EqualOptions): boolean {
|
|
170
|
-
if (source === target) return true;
|
|
171
|
-
if (source == null || target == null) return false;
|
|
172
|
-
if (typeof source !== typeof target) return false;
|
|
173
|
-
|
|
174
|
-
if (source instanceof Date && target instanceof Date) {
|
|
175
|
-
return source.getTime() === target.getTime();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
(source instanceof DateTime && target instanceof DateTime) ||
|
|
180
|
-
(source instanceof DateOnly && target instanceof DateOnly) ||
|
|
181
|
-
(source instanceof Time && target instanceof Time)
|
|
182
|
-
) {
|
|
183
|
-
return source.tick === target.tick;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (source instanceof Uuid && target instanceof Uuid) {
|
|
187
|
-
return source.toString() === target.toString();
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (source instanceof RegExp && target instanceof RegExp) {
|
|
191
|
-
return source.source === target.source && source.flags === target.flags;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (source instanceof Array && target instanceof Array) {
|
|
195
|
-
return objEqualArray(source, target, options);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (source instanceof Map && target instanceof Map) {
|
|
199
|
-
return objEqualMap(source, target, options);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (source instanceof Set && target instanceof Set) {
|
|
203
|
-
return objEqualSet(source, target, options);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (typeof source === "object" && typeof target === "object") {
|
|
207
|
-
return objEqualObject(source as Record<string, unknown>, target as Record<string, unknown>, options);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function objEqualArray(source: unknown[], target: unknown[], options?: EqualOptions): boolean {
|
|
214
|
-
if (source.length !== target.length) {
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (options?.ignoreArrayIndex) {
|
|
219
|
-
const matchedIndices = new Set<number>();
|
|
220
|
-
|
|
221
|
-
if (options.onlyOneDepth) {
|
|
222
|
-
return source.every((sourceItem) => {
|
|
223
|
-
const idx = target.findIndex((t, i) => !matchedIndices.has(i) && t === sourceItem);
|
|
224
|
-
if (idx !== -1) {
|
|
225
|
-
matchedIndices.add(idx);
|
|
226
|
-
return true;
|
|
227
|
-
}
|
|
228
|
-
return false;
|
|
229
|
-
});
|
|
230
|
-
} else {
|
|
231
|
-
// 재귀 호출 시 topLevelIncludes/topLevelExcludes 옵션은 최상위 레벨에만 적용되므로 제외
|
|
232
|
-
const recursiveOptions = { ignoreArrayIndex: options.ignoreArrayIndex, onlyOneDepth: options.onlyOneDepth };
|
|
233
|
-
return source.every((sourceItem) => {
|
|
234
|
-
const idx = target.findIndex((t, i) => !matchedIndices.has(i) && objEqual(t, sourceItem, recursiveOptions));
|
|
235
|
-
if (idx !== -1) {
|
|
236
|
-
matchedIndices.add(idx);
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
return false;
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
} else {
|
|
243
|
-
if (options?.onlyOneDepth) {
|
|
244
|
-
for (let i = 0; i < source.length; i++) {
|
|
245
|
-
if (source[i] !== target[i]) {
|
|
246
|
-
return false;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
} else {
|
|
250
|
-
// 재귀 호출 시 topLevelIncludes/topLevelExcludes 옵션은 최상위 레벨에만 적용되므로 제외
|
|
251
|
-
for (let i = 0; i < source.length; i++) {
|
|
252
|
-
if (
|
|
253
|
-
!objEqual(source[i], target[i], {
|
|
254
|
-
ignoreArrayIndex: options?.ignoreArrayIndex,
|
|
255
|
-
onlyOneDepth: options?.onlyOneDepth,
|
|
256
|
-
})
|
|
257
|
-
) {
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return true;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Map 객체 비교
|
|
269
|
-
* @note 비문자열 키(객체, 배열 등) 처리 시 O(n²) 복잡도 발생
|
|
270
|
-
* @note 대량 데이터의 경우 onlyOneDepth: true 옵션 사용 권장 (참조 비교로 O(n)으로 개선)
|
|
271
|
-
*/
|
|
272
|
-
function objEqualMap(source: Map<unknown, unknown>, target: Map<unknown, unknown>, options?: EqualOptions): boolean {
|
|
273
|
-
// Map 비교 시 topLevelIncludes/topLevelExcludes 옵션은 무시됨 (object 속성 키에만 적용)
|
|
274
|
-
const sourceKeys = Array.from(source.keys()).filter((key) => source.get(key) != null);
|
|
275
|
-
const targetKeys = Array.from(target.keys()).filter((key) => target.get(key) != null);
|
|
276
|
-
|
|
277
|
-
if (sourceKeys.length !== targetKeys.length) {
|
|
278
|
-
return false;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const usedTargetKeys = new Set<number>();
|
|
282
|
-
for (const sourceKey of sourceKeys) {
|
|
283
|
-
// 문자열 키: 직접 비교
|
|
284
|
-
if (typeof sourceKey === "string") {
|
|
285
|
-
const sourceValue = source.get(sourceKey);
|
|
286
|
-
const targetValue = target.get(sourceKey);
|
|
287
|
-
if (options?.onlyOneDepth) {
|
|
288
|
-
if (sourceValue !== targetValue) return false;
|
|
289
|
-
} else {
|
|
290
|
-
if (
|
|
291
|
-
!objEqual(sourceValue, targetValue, {
|
|
292
|
-
ignoreArrayIndex: options?.ignoreArrayIndex,
|
|
293
|
-
onlyOneDepth: options?.onlyOneDepth,
|
|
294
|
-
})
|
|
295
|
-
) {
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
} else {
|
|
300
|
-
// 비문자열 키: targetKeys에서 동등한 키 찾기
|
|
301
|
-
let found = false;
|
|
302
|
-
for (let i = 0; i < targetKeys.length; i++) {
|
|
303
|
-
const targetKey = targetKeys[i];
|
|
304
|
-
if (typeof targetKey === "string" || usedTargetKeys.has(i)) continue;
|
|
305
|
-
if (options?.onlyOneDepth ? sourceKey === targetKey : objEqual(sourceKey, targetKey)) {
|
|
306
|
-
usedTargetKeys.add(i);
|
|
307
|
-
const sourceValue = source.get(sourceKey);
|
|
308
|
-
const targetValue = target.get(targetKey);
|
|
309
|
-
if (options?.onlyOneDepth) {
|
|
310
|
-
if (sourceValue !== targetValue) return false;
|
|
311
|
-
} else {
|
|
312
|
-
if (
|
|
313
|
-
!objEqual(sourceValue, targetValue, {
|
|
314
|
-
ignoreArrayIndex: options?.ignoreArrayIndex,
|
|
315
|
-
onlyOneDepth: options?.onlyOneDepth,
|
|
316
|
-
})
|
|
317
|
-
) {
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
found = true;
|
|
322
|
-
break;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
if (!found) return false;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return true;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function objEqualObject(
|
|
333
|
-
source: Record<string, unknown>,
|
|
334
|
-
target: Record<string, unknown>,
|
|
335
|
-
options?: EqualOptions,
|
|
336
|
-
): boolean {
|
|
337
|
-
const sourceKeys = Object.keys(source).filter(
|
|
338
|
-
(key) =>
|
|
339
|
-
(options?.topLevelIncludes === undefined || options.topLevelIncludes.includes(key)) &&
|
|
340
|
-
!options?.topLevelExcludes?.includes(key) &&
|
|
341
|
-
source[key] != null,
|
|
342
|
-
);
|
|
343
|
-
const targetKeys = Object.keys(target).filter(
|
|
344
|
-
(key) =>
|
|
345
|
-
(options?.topLevelIncludes === undefined || options.topLevelIncludes.includes(key)) &&
|
|
346
|
-
!options?.topLevelExcludes?.includes(key) &&
|
|
347
|
-
target[key] != null,
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
if (sourceKeys.length !== targetKeys.length) {
|
|
351
|
-
return false;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
for (const key of sourceKeys) {
|
|
355
|
-
if (options?.onlyOneDepth) {
|
|
356
|
-
if (source[key] !== target[key]) {
|
|
357
|
-
return false;
|
|
358
|
-
}
|
|
359
|
-
} else {
|
|
360
|
-
if (
|
|
361
|
-
!objEqual(source[key], target[key], {
|
|
362
|
-
ignoreArrayIndex: options?.ignoreArrayIndex,
|
|
363
|
-
})
|
|
364
|
-
) {
|
|
365
|
-
return false;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return true;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Set 깊은 비교
|
|
375
|
-
* @note deep equal 비교(`onlyOneDepth: false`)는 O(n²) 시간 복잡도를 가짐.
|
|
376
|
-
* primitive Set이나 성능이 중요한 경우 `onlyOneDepth: true` 사용 권장
|
|
377
|
-
*/
|
|
378
|
-
function objEqualSet(source: Set<unknown>, target: Set<unknown>, options?: EqualOptions): boolean {
|
|
379
|
-
if (source.size !== target.size) {
|
|
380
|
-
return false;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (options?.onlyOneDepth) {
|
|
384
|
-
for (const sourceItem of source) {
|
|
385
|
-
if (!target.has(sourceItem)) {
|
|
386
|
-
return false;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
} else {
|
|
390
|
-
// deep equal: target 배열을 루프 외부에서 1회만 생성
|
|
391
|
-
// 매칭된 인덱스를 추적하여 중복 매칭 방지
|
|
392
|
-
const targetArr = [...target];
|
|
393
|
-
const matchedIndices = new Set<number>();
|
|
394
|
-
for (const sourceItem of source) {
|
|
395
|
-
const idx = targetArr.findIndex((t, i) => !matchedIndices.has(i) && objEqual(sourceItem, t, options));
|
|
396
|
-
if (idx === -1) {
|
|
397
|
-
return false;
|
|
398
|
-
}
|
|
399
|
-
matchedIndices.add(idx);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return true;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
//#endregion
|
|
407
|
-
|
|
408
|
-
//#region objMerge
|
|
409
|
-
|
|
410
|
-
/** objMerge 옵션 타입 */
|
|
411
|
-
export interface ObjMergeOptions {
|
|
412
|
-
/** 배열 처리 방식. "replace": target으로 대체(기본), "concat": 합침(중복 제거) */
|
|
413
|
-
arrayProcess?: "replace" | "concat";
|
|
414
|
-
/** target이 null일 때 해당 키 삭제 여부 */
|
|
415
|
-
useDelTargetNull?: boolean;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* 깊은 병합 (source를 base로 target을 병합)
|
|
420
|
-
*
|
|
421
|
-
* @param source 기준 객체
|
|
422
|
-
* @param target 병합할 객체
|
|
423
|
-
* @param opt 병합 옵션
|
|
424
|
-
* @param opt.arrayProcess 배열 처리 방식
|
|
425
|
-
* - `"replace"`: target 배열로 대체 (기본값)
|
|
426
|
-
* - `"concat"`: source와 target 배열을 합침 (Set으로 중복 제거)
|
|
427
|
-
* @param opt.useDelTargetNull target 값이 null일 때 해당 키 삭제 여부
|
|
428
|
-
* - `true`: target이 null이면 결과에서 해당 키 삭제
|
|
429
|
-
* - `false` 또는 미지정: source 값 유지
|
|
430
|
-
*
|
|
431
|
-
* @note 원본 객체를 수정하지 않고 새 객체를 반환함 (불변성 보장)
|
|
432
|
-
* @note arrayProcess="concat" 사용 시 Set을 통해 중복을 제거하며,
|
|
433
|
-
* 객체 배열의 경우 참조(주소) 비교로 중복을 판단함
|
|
434
|
-
* @note 타입이 다른 경우 target 값으로 덮어씀
|
|
435
|
-
*/
|
|
436
|
-
export function objMerge<T, P>(source: T, target: P, opt?: ObjMergeOptions): T & P {
|
|
437
|
-
if (source == null) {
|
|
438
|
-
return objClone(target) as T & P;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (target === undefined) {
|
|
442
|
-
return objClone(source) as T & P;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (target === null) {
|
|
446
|
-
return opt?.useDelTargetNull ? (undefined as T & P) : (objClone(source) as T & P);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (typeof target !== "object") {
|
|
450
|
-
return target as T & P;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (
|
|
454
|
-
target instanceof Date ||
|
|
455
|
-
target instanceof DateTime ||
|
|
456
|
-
target instanceof DateOnly ||
|
|
457
|
-
target instanceof Time ||
|
|
458
|
-
target instanceof Uuid ||
|
|
459
|
-
target instanceof Uint8Array ||
|
|
460
|
-
(opt?.arrayProcess === "replace" && target instanceof Array)
|
|
461
|
-
) {
|
|
462
|
-
return objClone(target) as T & P;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// source가 object가 아니거나, source와 target이 다른 종류의 object면 target으로 덮어씀
|
|
466
|
-
if (typeof source !== "object" || source.constructor !== target.constructor) {
|
|
467
|
-
return objClone(target) as T & P;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (source instanceof Map && target instanceof Map) {
|
|
471
|
-
const result = objClone(source);
|
|
472
|
-
for (const key of target.keys()) {
|
|
473
|
-
if (result.has(key)) {
|
|
474
|
-
result.set(key, objMerge(result.get(key), target.get(key), opt));
|
|
475
|
-
} else {
|
|
476
|
-
result.set(key, objClone(target.get(key)));
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
return result as T & P;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (opt?.arrayProcess === "concat" && source instanceof Array && target instanceof Array) {
|
|
483
|
-
let result = [...new Set([...source, ...target])];
|
|
484
|
-
if (opt.useDelTargetNull) {
|
|
485
|
-
result = result.filter((item) => item !== null);
|
|
486
|
-
}
|
|
487
|
-
return result as T & P;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
const sourceRec = source as Record<string, unknown>;
|
|
491
|
-
const targetRec = target as Record<string, unknown>;
|
|
492
|
-
const resultRec = objClone(sourceRec);
|
|
493
|
-
for (const key of Object.keys(target)) {
|
|
494
|
-
resultRec[key] = objMerge(sourceRec[key], targetRec[key], opt);
|
|
495
|
-
if (resultRec[key] === undefined) {
|
|
496
|
-
delete resultRec[key];
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
return resultRec as T & P;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/** merge3 옵션 타입 */
|
|
504
|
-
export interface ObjMerge3KeyOptions {
|
|
505
|
-
/** 비교할 하위 키 목록 (equal의 topLevelIncludes와 동일) */
|
|
506
|
-
keys?: string[];
|
|
507
|
-
/** 비교에서 제외할 하위 키 목록 */
|
|
508
|
-
excludes?: string[];
|
|
509
|
-
/** 배열 순서 무시 여부 */
|
|
510
|
-
ignoreArrayIndex?: boolean;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* 3-way 병합
|
|
515
|
-
*
|
|
516
|
-
* source, origin, target 세 객체를 비교하여 병합합니다.
|
|
517
|
-
* - source와 origin이 같고 target이 다르면 → target 값 사용
|
|
518
|
-
* - target과 origin이 같고 source가 다르면 → source 값 사용
|
|
519
|
-
* - source와 target이 같으면 → 해당 값 사용
|
|
520
|
-
* - 세 값이 모두 다르면 → 충돌 발생 (origin 값 유지)
|
|
521
|
-
*
|
|
522
|
-
* @param source 변경된 버전 1
|
|
523
|
-
* @param origin 기준 버전 (공통 조상)
|
|
524
|
-
* @param target 변경된 버전 2
|
|
525
|
-
* @param optionsObj 키별 비교 옵션. 각 키에 대해 equal() 비교 옵션을 개별 지정
|
|
526
|
-
* - `keys`: 비교할 하위 키 목록 (equal의 topLevelIncludes와 동일)
|
|
527
|
-
* - `excludes`: 비교에서 제외할 하위 키 목록
|
|
528
|
-
* - `ignoreArrayIndex`: 배열 순서 무시 여부
|
|
529
|
-
* @returns conflict: 충돌 발생 여부, result: 병합 결과
|
|
530
|
-
*
|
|
531
|
-
* @example
|
|
532
|
-
* const { conflict, result } = merge3(
|
|
533
|
-
* { a: 1, b: 2 }, // source
|
|
534
|
-
* { a: 1, b: 1 }, // origin
|
|
535
|
-
* { a: 2, b: 1 }, // target
|
|
536
|
-
* );
|
|
537
|
-
* // conflict: false, result: { a: 2, b: 2 }
|
|
538
|
-
*/
|
|
539
|
-
export function objMerge3<
|
|
540
|
-
S extends Record<string, unknown>,
|
|
541
|
-
O extends Record<string, unknown>,
|
|
542
|
-
T extends Record<string, unknown>,
|
|
543
|
-
>(
|
|
544
|
-
source: S,
|
|
545
|
-
origin: O,
|
|
546
|
-
target: T,
|
|
547
|
-
optionsObj?: Record<string, ObjMerge3KeyOptions>,
|
|
548
|
-
): {
|
|
549
|
-
conflict: boolean;
|
|
550
|
-
result: O & S & T;
|
|
551
|
-
} {
|
|
552
|
-
let conflict = false;
|
|
553
|
-
const result = objClone(origin) as Record<string, unknown>;
|
|
554
|
-
const allKeys = new Set([...Object.keys(source), ...Object.keys(target), ...Object.keys(origin)]);
|
|
555
|
-
for (const key of allKeys) {
|
|
556
|
-
if (objEqual(source[key], result[key], optionsObj?.[key])) {
|
|
557
|
-
result[key] = objClone(target[key]);
|
|
558
|
-
} else if (objEqual(target[key], result[key], optionsObj?.[key])) {
|
|
559
|
-
result[key] = objClone(source[key]);
|
|
560
|
-
} else if (objEqual(source[key], target[key], optionsObj?.[key])) {
|
|
561
|
-
result[key] = objClone(source[key]);
|
|
562
|
-
} else {
|
|
563
|
-
conflict = true;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return {
|
|
568
|
-
conflict,
|
|
569
|
-
result: result as O & S & T,
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
//#endregion
|
|
574
|
-
|
|
575
|
-
//#region objOmit / objPick
|
|
576
|
-
|
|
577
|
-
/**
|
|
578
|
-
* 객체에서 특정 키들을 제외
|
|
579
|
-
* @param item 원본 객체
|
|
580
|
-
* @param omitKeys 제외할 키 배열
|
|
581
|
-
* @returns 지정된 키가 제외된 새 객체
|
|
582
|
-
* @example
|
|
583
|
-
* const user = { name: "Alice", age: 30, email: "alice@example.com" };
|
|
584
|
-
* objOmit(user, ["email"]);
|
|
585
|
-
* // { name: "Alice", age: 30 }
|
|
586
|
-
*/
|
|
587
|
-
export function objOmit<T extends Record<string, unknown>, K extends keyof T>(item: T, omitKeys: K[]): Omit<T, K> {
|
|
588
|
-
const result: Record<string, unknown> = {};
|
|
589
|
-
for (const key of Object.keys(item)) {
|
|
590
|
-
if (!omitKeys.includes(key as K)) {
|
|
591
|
-
result[key] = item[key];
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
return result as Omit<T, K>;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* 조건에 맞는 키들을 제외
|
|
599
|
-
* @internal
|
|
600
|
-
* @param item 원본 객체
|
|
601
|
-
* @param omitKeyFn 키를 받아 제외 여부를 반환하는 함수 (true면 제외)
|
|
602
|
-
* @returns 조건에 맞는 키가 제외된 새 객체
|
|
603
|
-
* @example
|
|
604
|
-
* const data = { name: "Alice", _internal: "secret", age: 30 };
|
|
605
|
-
* objOmitByFilter(data, (key) => key.startsWith("_"));
|
|
606
|
-
* // { name: "Alice", age: 30 }
|
|
607
|
-
*/
|
|
608
|
-
export function objOmitByFilter<T extends Record<string, unknown>>(item: T, omitKeyFn: (key: keyof T) => boolean): T {
|
|
609
|
-
const result: Record<string, unknown> = {};
|
|
610
|
-
for (const key of Object.keys(item)) {
|
|
611
|
-
if (!omitKeyFn(key)) {
|
|
612
|
-
result[key] = item[key];
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
return result as T;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/**
|
|
619
|
-
* 객체에서 특정 키들만 선택
|
|
620
|
-
* @param item 원본 객체
|
|
621
|
-
* @param keys 선택할 키 배열
|
|
622
|
-
* @returns 지정된 키만 포함된 새 객체
|
|
623
|
-
* @example
|
|
624
|
-
* const user = { name: "Alice", age: 30, email: "alice@example.com" };
|
|
625
|
-
* objPick(user, ["name", "age"]);
|
|
626
|
-
* // { name: "Alice", age: 30 }
|
|
627
|
-
*/
|
|
628
|
-
export function objPick<T extends Record<string, unknown>, K extends keyof T>(item: T, keys: K[]): Pick<T, K> {
|
|
629
|
-
const result: Record<string, unknown> = {};
|
|
630
|
-
for (const key of keys) {
|
|
631
|
-
result[key as string] = item[key];
|
|
632
|
-
}
|
|
633
|
-
return result as Pick<T, K>;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
//#endregion
|
|
637
|
-
|
|
638
|
-
//#region objGetChainValue / objSetChainValue / objDeleteChainValue
|
|
639
|
-
|
|
640
|
-
// 정규식 캐싱 (모듈 로드 시 1회만 생성)
|
|
641
|
-
const chainSplitRegex = /[.[\]]/g;
|
|
642
|
-
const chainCleanRegex = /[?!'"]/g;
|
|
643
|
-
const chainNumericRegex = /^[0-9]*$/;
|
|
644
|
-
|
|
645
|
-
function getChainSplits(chain: string): (string | number)[] {
|
|
646
|
-
const split = chain
|
|
647
|
-
.split(chainSplitRegex)
|
|
648
|
-
.map((item) => item.replace(chainCleanRegex, ""))
|
|
649
|
-
.filter((item) => Boolean(item));
|
|
650
|
-
const result: (string | number)[] = [];
|
|
651
|
-
for (const splitItem of split) {
|
|
652
|
-
if (chainNumericRegex.test(splitItem)) {
|
|
653
|
-
result.push(Number.parseInt(splitItem));
|
|
654
|
-
} else {
|
|
655
|
-
result.push(splitItem);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
return result;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
/**
|
|
663
|
-
* 체인 경로로 값 가져오기
|
|
664
|
-
* @example objGetChainValue(obj, "a.b[0].c")
|
|
665
|
-
*/
|
|
666
|
-
export function objGetChainValue(obj: unknown, chain: string, optional: true): unknown | undefined;
|
|
667
|
-
export function objGetChainValue(obj: unknown, chain: string): unknown;
|
|
668
|
-
export function objGetChainValue(obj: unknown, chain: string, optional?: true): unknown | undefined {
|
|
669
|
-
const splits = getChainSplits(chain);
|
|
670
|
-
let result: unknown = obj;
|
|
671
|
-
for (const splitItem of splits) {
|
|
672
|
-
if (optional && result === undefined) {
|
|
673
|
-
result = undefined;
|
|
674
|
-
} else {
|
|
675
|
-
result = (result as Record<string | number, unknown>)[splitItem];
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
return result;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
/**
|
|
682
|
-
* depth만큼 같은 키로 내려가기
|
|
683
|
-
* @internal
|
|
684
|
-
* @param obj 대상 객체
|
|
685
|
-
* @param key 내려갈 키
|
|
686
|
-
* @param depth 내려갈 깊이 (1 이상)
|
|
687
|
-
* @param optional true면 중간에 null/undefined가 있어도 에러 없이 undefined 반환
|
|
688
|
-
* @throws ArgumentError depth가 1 미만일 경우
|
|
689
|
-
* @example objGetChainValueByDepth({ parent: { parent: { name: 'a' } } }, 'parent', 2) => { name: 'a' }
|
|
690
|
-
*/
|
|
691
|
-
export function objGetChainValueByDepth<T, K extends keyof T>(
|
|
692
|
-
obj: T,
|
|
693
|
-
key: K,
|
|
694
|
-
depth: number,
|
|
695
|
-
optional: true,
|
|
696
|
-
): T[K] | undefined;
|
|
697
|
-
export function objGetChainValueByDepth<T, K extends keyof T>(obj: T, key: K, depth: number): T[K];
|
|
698
|
-
export function objGetChainValueByDepth<T, K extends keyof T>(
|
|
699
|
-
obj: T,
|
|
700
|
-
key: K,
|
|
701
|
-
depth: number,
|
|
702
|
-
optional?: true,
|
|
703
|
-
): T[K] | undefined {
|
|
704
|
-
if (depth < 1) {
|
|
705
|
-
throw new ArgumentError("depth는 1 이상이어야 합니다.", { depth });
|
|
706
|
-
}
|
|
707
|
-
let result: unknown = obj;
|
|
708
|
-
for (let i = 0; i < depth; i++) {
|
|
709
|
-
if (optional && result == null) {
|
|
710
|
-
result = undefined;
|
|
711
|
-
} else {
|
|
712
|
-
result = (result as Record<string, unknown>)[key as string];
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
return result as T[K] | undefined;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
/**
|
|
719
|
-
* 체인 경로로 값 설정
|
|
720
|
-
* @example objSetChainValue(obj, "a.b[0].c", value)
|
|
721
|
-
*/
|
|
722
|
-
export function objSetChainValue(obj: unknown, chain: string, value: unknown): void {
|
|
723
|
-
const splits = getChainSplits(chain);
|
|
724
|
-
if (splits.length === 0) {
|
|
725
|
-
throw new ArgumentError("체인이 비어있습니다.", { chain });
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
let curr: Record<string | number, unknown> = obj as Record<string | number, unknown>;
|
|
729
|
-
for (const splitItem of splits.slice(0, -1)) {
|
|
730
|
-
curr[splitItem] = curr[splitItem] ?? {};
|
|
731
|
-
curr = curr[splitItem] as Record<string | number, unknown>;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
const last = splits[splits.length - 1];
|
|
735
|
-
curr[last] = value;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
/**
|
|
739
|
-
* 체인 경로의 값 삭제
|
|
740
|
-
* @example objDeleteChainValue(obj, "a.b[0].c")
|
|
741
|
-
*/
|
|
742
|
-
export function objDeleteChainValue(obj: unknown, chain: string): void {
|
|
743
|
-
const splits = getChainSplits(chain);
|
|
744
|
-
if (splits.length === 0) {
|
|
745
|
-
throw new ArgumentError("체인이 비어있습니다.", { chain });
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
let curr: Record<string | number, unknown> = obj as Record<string | number, unknown>;
|
|
749
|
-
for (const splitItem of splits.slice(0, -1)) {
|
|
750
|
-
const next = curr[splitItem];
|
|
751
|
-
// 중간 경로가 없으면 조용히 리턴 (삭제할 것이 없음)
|
|
752
|
-
if (next == null || typeof next !== "object") {
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
curr = next as Record<string | number, unknown>;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
const last = splits[splits.length - 1];
|
|
759
|
-
delete curr[last];
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
//#endregion
|
|
763
|
-
|
|
764
|
-
//#region objClearUndefined / objClear / objNullToUndefined / objUnflatten
|
|
765
|
-
|
|
766
|
-
/**
|
|
767
|
-
* 객체에서 undefined 값을 가진 키 삭제
|
|
768
|
-
* @internal
|
|
769
|
-
*
|
|
770
|
-
* @mutates 원본 객체를 직접 수정함
|
|
771
|
-
*/
|
|
772
|
-
export function objClearUndefined<T extends object>(obj: T): T {
|
|
773
|
-
const record = obj as Record<string, unknown>;
|
|
774
|
-
for (const key of Object.keys(record)) {
|
|
775
|
-
if (record[key] === undefined) {
|
|
776
|
-
delete record[key];
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
return obj;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
* 객체의 모든 키 삭제
|
|
784
|
-
* @internal
|
|
785
|
-
*
|
|
786
|
-
* @mutates 원본 객체를 직접 수정함
|
|
787
|
-
*/
|
|
788
|
-
export function objClear<T extends Record<string, unknown>>(obj: T): Record<string, never> {
|
|
789
|
-
for (const key of Object.keys(obj)) {
|
|
790
|
-
delete obj[key];
|
|
791
|
-
}
|
|
792
|
-
return obj as Record<string, never>;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* null을 undefined로 변환 (재귀적)
|
|
797
|
-
* @internal
|
|
798
|
-
*
|
|
799
|
-
* @mutates 원본 배열/객체를 직접 수정함
|
|
800
|
-
*/
|
|
801
|
-
export function objNullToUndefined<T>(obj: T): T | undefined {
|
|
802
|
-
return objNullToUndefinedImpl(obj, new WeakSet());
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
function objNullToUndefinedImpl<T>(obj: T, seen: WeakSet<object>): T | undefined {
|
|
806
|
-
if (obj == null) {
|
|
807
|
-
return undefined;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
if (
|
|
811
|
-
obj instanceof Date ||
|
|
812
|
-
obj instanceof DateTime ||
|
|
813
|
-
obj instanceof DateOnly ||
|
|
814
|
-
obj instanceof Time ||
|
|
815
|
-
obj instanceof Uuid
|
|
816
|
-
) {
|
|
817
|
-
return obj;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
if (obj instanceof Array) {
|
|
821
|
-
if (seen.has(obj)) return obj;
|
|
822
|
-
seen.add(obj);
|
|
823
|
-
for (let i = 0; i < obj.length; i++) {
|
|
824
|
-
obj[i] = objNullToUndefinedImpl(obj[i], seen);
|
|
825
|
-
}
|
|
826
|
-
return obj;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
if (typeof obj === "object") {
|
|
830
|
-
if (seen.has(obj as object)) return obj;
|
|
831
|
-
seen.add(obj as object);
|
|
832
|
-
const objRec = obj as Record<string, unknown>;
|
|
833
|
-
for (const key of Object.keys(obj)) {
|
|
834
|
-
objRec[key] = objNullToUndefinedImpl(objRec[key], seen);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
return obj;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
return obj;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
/**
|
|
844
|
-
* flat된 객체를 nested 객체로 변환
|
|
845
|
-
* @internal
|
|
846
|
-
* @example objUnflatten({ "a.b.c": 1 }) => { a: { b: { c: 1 } } }
|
|
847
|
-
*/
|
|
848
|
-
export function objUnflatten(flatObj: Record<string, unknown>): Record<string, unknown> {
|
|
849
|
-
const result: Record<string, unknown> = {};
|
|
850
|
-
|
|
851
|
-
for (const key in flatObj) {
|
|
852
|
-
const parts = key.split(".");
|
|
853
|
-
let current: Record<string, unknown> = result;
|
|
854
|
-
|
|
855
|
-
for (let i = 0; i < parts.length; i++) {
|
|
856
|
-
const part = parts[i];
|
|
857
|
-
|
|
858
|
-
if (i === parts.length - 1) {
|
|
859
|
-
current[part] = flatObj[key];
|
|
860
|
-
} else {
|
|
861
|
-
if (!(part in current)) {
|
|
862
|
-
current[part] = {};
|
|
863
|
-
}
|
|
864
|
-
current = current[part] as Record<string, unknown>;
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
return result;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
//#endregion
|
|
873
|
-
|
|
874
|
-
//#region 타입 유틸리티
|
|
875
|
-
|
|
876
|
-
/**
|
|
877
|
-
* undefined를 가진 프로퍼티를 optional로 변환
|
|
878
|
-
* @example { a: string; b: string | undefined } → { a: string; b?: string | undefined }
|
|
879
|
-
*/
|
|
880
|
-
export type ObjUndefToOptional<T> = {
|
|
881
|
-
[K in keyof T as undefined extends T[K] ? K : never]?: T[K];
|
|
882
|
-
} & { [K in keyof T as undefined extends T[K] ? never : K]: T[K] };
|
|
883
|
-
|
|
884
|
-
/**
|
|
885
|
-
* optional 프로퍼티를 required + undefined 유니온으로 변환
|
|
886
|
-
* @example { a: string; b?: string } → { a: string; b: string | undefined }
|
|
887
|
-
*/
|
|
888
|
-
export type ObjOptionalToUndef<T> = {
|
|
889
|
-
[K in keyof T]-?: {} extends Pick<T, K> ? T[K] | undefined : T[K];
|
|
890
|
-
};
|
|
891
|
-
|
|
892
|
-
//#endregion
|
|
893
|
-
|
|
894
|
-
/**
|
|
895
|
-
* Object.keys의 타입 안전한 버전
|
|
896
|
-
* @param obj 키를 추출할 객체
|
|
897
|
-
* @returns 객체의 키 배열
|
|
898
|
-
*/
|
|
899
|
-
export function objKeys<T extends object>(obj: T): (keyof T)[] {
|
|
900
|
-
return Object.keys(obj) as (keyof T)[];
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
/**
|
|
904
|
-
* Object.entries의 타입 안전한 버전
|
|
905
|
-
* @param obj 엔트리를 추출할 객체
|
|
906
|
-
* @returns [키, 값] 튜플 배열
|
|
907
|
-
*/
|
|
908
|
-
export function objEntries<T extends object>(obj: T): ObjEntries<T> {
|
|
909
|
-
return Object.entries(obj) as ObjEntries<T>;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
/**
|
|
913
|
-
* Object.fromEntries의 타입 안전한 버전
|
|
914
|
-
* @param entries [키, 값] 튜플 배열
|
|
915
|
-
* @returns 생성된 객체
|
|
916
|
-
*/
|
|
917
|
-
export function objFromEntries<T extends [string, unknown]>(entries: T[]): { [K in T[0]]: T[1] } {
|
|
918
|
-
return Object.fromEntries(entries) as { [K in T[0]]: T[1] };
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
type ObjEntries<T> = { [K in keyof T]: [K, T[K]] }[keyof T][];
|
|
922
|
-
|
|
923
|
-
/**
|
|
924
|
-
* 객체의 각 엔트리를 변환하여 새 객체 반환
|
|
925
|
-
* @param obj 변환할 객체
|
|
926
|
-
* @param fn 변환 함수 (key, value) => [newKey, newValue]
|
|
927
|
-
* @returns 변환된 키와 값을 가진 새 객체
|
|
928
|
-
* @example
|
|
929
|
-
* const colors = { primary: "255, 0, 0", secondary: "0, 255, 0" };
|
|
930
|
-
*
|
|
931
|
-
* // 값만 변환
|
|
932
|
-
* objMap(colors, (key, rgb) => [null, `rgb(${rgb})`]);
|
|
933
|
-
* // { primary: "rgb(255, 0, 0)", secondary: "rgb(0, 255, 0)" }
|
|
934
|
-
*
|
|
935
|
-
* // 키와 값 모두 변환
|
|
936
|
-
* objMap(colors, (key, rgb) => [`${key}Light`, `rgb(${rgb})`]);
|
|
937
|
-
* // { primaryLight: "rgb(255, 0, 0)", secondaryLight: "rgb(0, 255, 0)" }
|
|
938
|
-
*/
|
|
939
|
-
export function objMap<T extends object, NK extends string, NV>(
|
|
940
|
-
obj: T,
|
|
941
|
-
fn: (key: keyof T, value: T[keyof T]) => [NK | null, NV],
|
|
942
|
-
): Record<NK | Extract<keyof T, string>, NV> {
|
|
943
|
-
return objMapImpl(obj, fn);
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
function objMapImpl<T extends object, NK extends string, NV>(
|
|
947
|
-
obj: T,
|
|
948
|
-
fn: (key: keyof T, value: T[keyof T]) => [NK | null, NV],
|
|
949
|
-
): Record<string, NV> {
|
|
950
|
-
const result: Record<string, NV> = {};
|
|
951
|
-
for (const key of Object.keys(obj)) {
|
|
952
|
-
const [newKey, newValue] = fn(key as keyof T, (obj as Record<string, T[keyof T]>)[key]);
|
|
953
|
-
result[newKey ?? key] = newValue;
|
|
954
|
-
}
|
|
955
|
-
return result;
|
|
956
|
-
}
|