Skip to content

Commit 9c2a4d2

Browse files
committed
refactor: improve type safety by replacing any with unknown and proper types
- error.ts: use unknown instead of any for error parameters - promise.ts: use unknown for error callbacks with proper type narrowing - object.ts: use Record<string, unknown> and proper generic types - split-data.ts: add JsonValue type, use unknown for public API - org-billing.ts: document WithSerializableTransactionFn any usage
1 parent 85d87d7 commit 9c2a4d2

File tree

7 files changed

+96
-53
lines changed

7 files changed

+96
-53
lines changed

common/src/util/__tests__/promise.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ describe('withRetry', () => {
299299

300300
const result = await withRetry(operation, {
301301
maxRetries: 3,
302-
retryIf: (error) => error?.code === 'RETRY_ME',
302+
retryIf: (error) => (error as { code?: string })?.code === 'RETRY_ME',
303303
})
304304

305305
expect(result).toBe('success')
@@ -315,7 +315,7 @@ describe('withRetry', () => {
315315
await expect(
316316
withRetry(operation, {
317317
maxRetries: 3,
318-
retryIf: (err) => err?.code === 'RETRY_ME',
318+
retryIf: (err) => (err as { code?: string })?.code === 'RETRY_ME',
319319
}),
320320
).rejects.toMatchObject({ code: 'DO_NOT_RETRY' })
321321

common/src/util/__tests__/split-data.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe('splitData - base cases', () => {
2121
it('splits short strings when maxChunkSize is small', () => {
2222
const input = { msg: 'abcdef'.repeat(10) } // 60 chars
2323

24-
const chunks = splitData({ data: input, maxChunkSize: 30 })
24+
const chunks = splitData({ data: input, maxChunkSize: 30 }) as { msg?: string }[]
2525

2626
expect(chunks.length).toBeGreaterThan(1)
2727
const combined = chunks.map((c) => c.msg).join('')
@@ -32,7 +32,7 @@ describe('splitData - base cases', () => {
3232
it('splits deeply nested strings with small maxChunkSize', () => {
3333
const input = { a: { b: { c: 'xyz123'.repeat(10) } } }
3434

35-
const chunks = splitData({ data: input, maxChunkSize: 50 })
35+
const chunks = splitData({ data: input, maxChunkSize: 50 }) as { a?: { b?: { c?: string } } }[]
3636

3737
expect(chunks.length).toBeGreaterThan(1)
3838
const reconstructed = chunks.map((c) => c.a?.b?.c ?? '').join('')
@@ -60,7 +60,7 @@ describe('splitData - base cases', () => {
6060
str: 'hello world'.repeat(5),
6161
}
6262

63-
const chunks = splitData({ data: input, maxChunkSize: 50 })
63+
const chunks = splitData({ data: input, maxChunkSize: 50 }) as { flag?: boolean; num?: number; str?: string }[]
6464

6565
expect(chunks.length).toBeGreaterThan(1)
6666
expect(chunks.every((c) => JSON.stringify(c).length <= 50)).toBe(true)
@@ -74,7 +74,7 @@ describe('splitData - base cases', () => {
7474
a: 'A'.repeat(20),
7575
b: 'B'.repeat(20),
7676
}
77-
const chunks = splitData({ data: input, maxChunkSize: 30 })
77+
const chunks = splitData({ data: input, maxChunkSize: 30 }) as { a?: string; b?: string }[]
7878

7979
expect(chunks.length).toBeGreaterThan(1)
8080

@@ -89,7 +89,7 @@ describe('splitData - base cases', () => {
8989
describe('splitData - array and string-specific splitting', () => {
9090
it('splits long strings into smaller string chunks', () => {
9191
const input = '12345678901234567890'
92-
const chunks = splitData({ data: input, maxChunkSize: 5 })
92+
const chunks = splitData({ data: input, maxChunkSize: 5 }) as string[]
9393

9494
expect(Array.isArray(chunks)).toBe(true)
9595
chunks.forEach((chunk) => {
@@ -121,7 +121,7 @@ describe('splitData - array and string-specific splitting', () => {
121121
b: 'bbb'.repeat(10),
122122
}
123123
const maxSize = 40
124-
const chunks = splitData({ data: input, maxChunkSize: maxSize })
124+
const chunks = splitData({ data: input, maxChunkSize: maxSize }) as { a?: string; b?: string }[]
125125

126126
expect(Array.isArray(chunks)).toBe(true)
127127
chunks.forEach((chunk) => {
@@ -162,7 +162,7 @@ describe('splitData - array and string-specific splitting', () => {
162162
]
163163
const maxSize = 30
164164

165-
const chunks = splitData({ data: input, maxChunkSize: maxSize })
165+
const chunks = splitData({ data: input, maxChunkSize: maxSize }) as { msg?: string; val?: number }[][]
166166

167167
expect(Array.isArray(chunks)).toBe(true)
168168
chunks.forEach((chunk) => {

common/src/util/error.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,29 +33,29 @@ export function success<T>(value: T): Success<T> {
3333
}
3434
}
3535

36-
export function failure(error: any): Failure<ErrorObject> {
36+
export function failure(error: unknown): Failure<ErrorObject> {
3737
return {
3838
success: false,
3939
error: getErrorObject(error),
4040
}
4141
}
4242

4343
export function getErrorObject(
44-
error: any,
44+
error: unknown,
4545
options: { includeRawError?: boolean } = {},
4646
): ErrorObject {
4747
if (error instanceof Error) {
48-
const anyError = error as any
48+
const errorWithExtras = error as { status?: unknown; statusCode?: unknown; code?: unknown }
4949
return {
5050
name: error.name,
5151
message: error.message,
5252
stack: error.stack,
53-
status: typeof anyError.status === 'number' ? anyError.status : undefined,
53+
status: typeof errorWithExtras.status === 'number' ? errorWithExtras.status : undefined,
5454
statusCode:
55-
typeof anyError.statusCode === 'number'
56-
? anyError.statusCode
55+
typeof errorWithExtras.statusCode === 'number'
56+
? errorWithExtras.statusCode
5757
: undefined,
58-
code: typeof anyError.code === 'string' ? anyError.code : undefined,
58+
code: typeof errorWithExtras.code === 'string' ? errorWithExtras.code : undefined,
5959
rawError: options.includeRawError
6060
? JSON.stringify(error, null, 2)
6161
: undefined,

common/src/util/object.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,45 @@
11
import { isEqual, mapValues, union } from 'lodash'
22

3+
type RemoveUndefined<T extends object> = {
4+
[K in keyof T as T[K] extends undefined ? never : K]: Exclude<T[K], undefined>
5+
}
6+
37
export const removeUndefinedProps = <T extends object>(
48
obj: T,
5-
): {
6-
[K in keyof T as T[K] extends undefined ? never : K]: Exclude<T[K], undefined>
7-
} => {
8-
const newObj: any = {}
9+
): RemoveUndefined<T> => {
10+
const newObj: Record<string, unknown> = {}
911

1012
for (const key of Object.keys(obj)) {
11-
if ((obj as any)[key] !== undefined) newObj[key] = (obj as any)[key]
13+
const value = obj[key as keyof T]
14+
if (value !== undefined) newObj[key] = value
1215
}
1316

14-
return newObj
17+
return newObj as RemoveUndefined<T>
1518
}
1619

1720
export const removeNullOrUndefinedProps = <T extends object>(
1821
obj: T,
1922
exceptions?: string[],
2023
): T => {
21-
const newObj: any = {}
24+
const newObj: Record<string, unknown> = {}
2225

2326
for (const key of Object.keys(obj)) {
27+
const value = obj[key as keyof T]
2428
if (
25-
((obj as any)[key] !== undefined && (obj as any)[key] !== null) ||
29+
(value !== undefined && value !== null) ||
2630
(exceptions ?? []).includes(key)
2731
)
28-
newObj[key] = (obj as any)[key]
32+
newObj[key] = value
2933
}
30-
return newObj
34+
return newObj as T
3135
}
3236

3337
export const addObjects = <T extends { [key: string]: number }>(
3438
obj1: T,
3539
obj2: T,
3640
) => {
3741
const keys = union(Object.keys(obj1), Object.keys(obj2))
38-
const newObj = {} as any
42+
const newObj: Record<string, number> = {}
3943

4044
for (const key of keys) {
4145
newObj[key] = (obj1[key] ?? 0) + (obj2[key] ?? 0)
@@ -49,7 +53,7 @@ export const subtractObjects = <T extends { [key: string]: number }>(
4953
obj2: T,
5054
) => {
5155
const keys = union(Object.keys(obj1), Object.keys(obj2))
52-
const newObj = {} as any
56+
const newObj: Record<string, number> = {}
5357

5458
for (const key of keys) {
5559
newObj[key] = (obj1[key] ?? 0) - (obj2[key] ?? 0)
@@ -68,14 +72,19 @@ export const hasSignificantDeepChanges = <T extends object>(
6872
partial: Partial<T>,
6973
epsilonForNumbers: number,
7074
): boolean => {
71-
const compareValues = (currValue: any, partialValue: any): boolean => {
75+
const compareValues = (currValue: unknown, partialValue: unknown): boolean => {
7276
if (typeof currValue === 'number' && typeof partialValue === 'number') {
7377
return Math.abs(currValue - partialValue) > epsilonForNumbers
7478
}
75-
if (typeof currValue === 'object' && typeof partialValue === 'object') {
79+
if (
80+
typeof currValue === 'object' &&
81+
currValue !== null &&
82+
typeof partialValue === 'object' &&
83+
partialValue !== null
84+
) {
7685
return hasSignificantDeepChanges(
77-
currValue,
78-
partialValue,
86+
currValue as Record<string, unknown>,
87+
partialValue as Partial<Record<string, unknown>>,
7988
epsilonForNumbers,
8089
)
8190
}
@@ -95,7 +104,7 @@ export const hasSignificantDeepChanges = <T extends object>(
95104

96105
export const filterObject = <T extends object>(
97106
obj: T,
98-
predicate: (value: any, key: keyof T) => boolean,
107+
predicate: (value: T[keyof T], key: keyof T) => boolean,
99108
): { [P in keyof T]: T[P] } => {
100109
const result = {} as { [P in keyof T]: T[P] }
101110
for (const key in obj) {

common/src/util/promise.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@ export async function withRetry<T>(
44
operation: () => Promise<T>,
55
options: {
66
maxRetries?: number
7-
retryIf?: (error: any) => boolean
8-
onRetry?: (error: any, attempt: number) => void
7+
retryIf?: (error: unknown) => boolean
8+
onRetry?: (error: unknown, attempt: number) => void
99
retryDelayMs?: number
1010
} = {},
1111
): Promise<T> {
1212
const {
1313
maxRetries = 3,
14-
retryIf = (error) => error?.type === 'APIConnectionError',
14+
retryIf = (error) => {
15+
const errorObj = error as { type?: string } | null | undefined
16+
return errorObj?.type === 'APIConnectionError'
17+
},
1518
onRetry = () => {},
1619
retryDelayMs = INITIAL_RETRY_DELAY,
1720
} = options
1821

19-
let lastError: any = null
22+
let lastError: unknown = null
2023

2124
for (let attempt = 0; attempt < maxRetries; attempt++) {
2225
try {

common/src/util/split-data.ts

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,32 @@
1-
type PlainObject = Record<string, any>
1+
/**
2+
* Represents any JSON-serializable value (primitives, arrays, or objects).
3+
* Used internally for type-safe JSON splitting operations.
4+
*/
5+
type JsonValue =
6+
| string
7+
| number
8+
| boolean
9+
| null
10+
| undefined
11+
| JsonValue[]
12+
| { [key: string]: JsonValue }
13+
14+
type PlainObject = Record<string, JsonValue>
215

316
interface Chunk<T> {
417
data: T
518
length: number
619
}
720

8-
function isPlainObject(val: any): val is PlainObject {
21+
function isPlainObject(val: unknown): val is PlainObject {
922
return (
1023
typeof val === 'object' &&
1124
val !== null &&
1225
Object.getPrototypeOf(val) === Object.prototype
1326
)
1427
}
1528

16-
function getJsonSize(data: any): number {
29+
function getJsonSize(data: unknown): number {
1730
if (data === undefined) {
1831
return 'undefined'.length
1932
}
@@ -93,7 +106,7 @@ function splitObject(params: {
93106
})
94107

95108
for (const [index, item] of items.entries()) {
96-
const itemWithKey: Chunk<any> = {
109+
const itemWithKey: Chunk<PlainObject> = {
97110
data: { [key]: item.data },
98111
length: item.length + overhead,
99112
}
@@ -155,14 +168,14 @@ function splitObject(params: {
155168
return chunks
156169
}
157170

158-
function splitArray(params: { arr: any[]; maxSize: number }): Chunk<any[]>[] {
171+
function splitArray(params: { arr: JsonValue[]; maxSize: number }): Chunk<JsonValue[]>[] {
159172
const { arr, maxSize } = params
160-
const chunks: Chunk<any[]>[] = []
161-
let currentChunk: Chunk<any[]> = { data: [], length: 2 }
173+
const chunks: Chunk<JsonValue[]>[] = []
174+
let currentChunk: Chunk<JsonValue[]> = { data: [], length: 2 }
162175

163176
for (const element of arr) {
164-
const entryArr = [element]
165-
const standaloneEntry: Chunk<any[]> = {
177+
const entryArr: JsonValue[] = [element]
178+
const standaloneEntry: Chunk<JsonValue[]> = {
166179
data: entryArr,
167180
length: getJsonSize(entryArr),
168181
}
@@ -224,27 +237,29 @@ function splitArray(params: { arr: any[]; maxSize: number }): Chunk<any[]>[] {
224237
}
225238

226239
function splitDataWithLengths(params: {
227-
data: any
240+
data: unknown
228241
maxChunkSize: number
229-
}): Chunk<any>[] {
242+
}): Chunk<JsonValue>[] {
230243
const { data, maxChunkSize } = params
231244
// Handle primitives
232245
if (typeof data !== 'object' || data === null) {
233246
if (typeof data === 'string') {
234247
const result = splitString({ data, maxSize: maxChunkSize })
235248
return result
236249
}
237-
return [{ data, length: getJsonSize(data) }]
250+
// Primitives (number, boolean, null, undefined) are valid JsonValues
251+
return [{ data: data as JsonValue, length: getJsonSize(data) }]
238252
}
239253

240-
// Non-plain objects (Date, RegExp, etc.)
254+
// Non-plain objects (Date, RegExp, etc.) - pass through as-is
255+
// These will be serialized by JSON.stringify when needed
241256
if (!Array.isArray(data) && !isPlainObject(data)) {
242-
return [{ data, length: getJsonSize(data) }]
257+
return [{ data: data as JsonValue, length: getJsonSize(data) }]
243258
}
244259

245260
// Arrays
246261
if (Array.isArray(data)) {
247-
const result = splitArray({ arr: data, maxSize: maxChunkSize })
262+
const result = splitArray({ arr: data as JsonValue[], maxSize: maxChunkSize })
248263
return result
249264
}
250265

@@ -253,7 +268,15 @@ function splitDataWithLengths(params: {
253268
return result
254269
}
255270

256-
export function splitData(params: { data: any; maxChunkSize?: number }): any[] {
271+
/**
272+
* Splits JSON-serializable data into smaller chunks that fit within the specified size limit.
273+
* Preserves the structure of objects and arrays while splitting long strings and nested values.
274+
*
275+
* @param params.data - The data to split (can be any JSON-serializable value)
276+
* @param params.maxChunkSize - Maximum size in characters for each chunk (default: 99,000)
277+
* @returns An array of chunks, each fitting within the size limit
278+
*/
279+
export function splitData(params: { data: unknown; maxChunkSize?: number }): unknown[] {
257280
const { data, maxChunkSize = 99_000 } = params
258281
return splitDataWithLengths({ data, maxChunkSize }).map((cwjl) => cwjl.data)
259282
}

packages/billing/src/org-billing.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,16 @@ export async function calculateOrganizationUsageAndBalance(
277277

278278
/**
279279
* Type for the withSerializableTransaction dependency.
280+
*
281+
* The callback parameter uses `any` for the same reason as `BillingTransactionFn`:
282+
* Drizzle's `PgTransaction` type has method signatures incompatible with our
283+
* simplified `BillingDbConnection` interface. Using `any` allows both real
284+
* Drizzle transactions and mock implementations to work.
285+
*
286+
* @see BillingTransactionFn in `@codebuff/common/types/contracts/billing` for details
280287
*/
281288
type WithSerializableTransactionFn = <T>(params: {
289+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
282290
callback: (tx: any) => Promise<T>
283291
context: Record<string, unknown>
284292
logger: Logger

0 commit comments

Comments
 (0)