Skip to content

Commit 5e4cad4

Browse files
committed
chore: wip
1 parent 053bde5 commit 5e4cad4

File tree

3 files changed

+173
-85
lines changed

3 files changed

+173
-85
lines changed

src/processor.ts

Lines changed: 145 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -381,15 +381,13 @@ export function processVariableDeclaration(decl: Declaration): string {
381381
// If we have a value, check if it has 'as const' - if so, infer from value instead of type annotation
382382
if (decl.value && decl.value.includes('as const')) {
383383
typeAnnotation = inferNarrowType(decl.value, true)
384-
} else if (decl.value && kind === 'const') {
385-
// For const declarations, always try to infer a more specific type from the value
386-
const inferredType = inferNarrowType(decl.value, false)
387-
388-
// Use the inferred type if it's more specific than a generic Record type
389-
if (!typeAnnotation ||
390-
typeAnnotation.startsWith('Record<') ||
391-
typeAnnotation === 'any' ||
392-
typeAnnotation === 'object') {
384+
} else if (!typeAnnotation && decl.value && kind === 'const') {
385+
// For const declarations WITHOUT explicit type annotation, infer narrow types from the value
386+
typeAnnotation = inferNarrowType(decl.value, true)
387+
} else if (typeAnnotation && decl.value && kind === 'const' && isGenericType(typeAnnotation)) {
388+
// For const declarations with generic type annotations (Record, any, object), prefer narrow inference
389+
const inferredType = inferNarrowType(decl.value, true)
390+
if (inferredType !== 'unknown') {
393391
typeAnnotation = inferredType
394392
}
395393
} else if (!typeAnnotation && decl.value) {
@@ -755,20 +753,14 @@ export function inferNarrowType(value: any, isConst: boolean = false): string {
755753
return 'string'
756754
}
757755

758-
// Number literals
756+
// Number literals - ALWAYS use literal types for const declarations
759757
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
760-
if (isConst) {
761-
return trimmed
762-
}
763-
return 'number'
758+
return trimmed // Always return literal number
764759
}
765760

766-
// Boolean literals
761+
// Boolean literals - ALWAYS use literal types for const declarations
767762
if (trimmed === 'true' || trimmed === 'false') {
768-
if (isConst) {
769-
return trimmed
770-
}
771-
return 'boolean'
763+
return trimmed // Always return literal boolean
772764
}
773765

774766
// Null and undefined
@@ -975,12 +967,24 @@ function inferArrayType(value: string, isConst: boolean): string {
975967
return inferNarrowTypeInUnion(trimmedEl, isConst)
976968
})
977969

978-
// For const arrays, create readonly tuples instead of union types
970+
// For const arrays, ALWAYS create readonly tuples for better type safety
979971
if (isConst) {
980972
return `readonly [${elementTypes.join(', ')}]`
981973
}
982974

975+
// For simple arrays with all same literal types, also create tuples
983976
const uniqueTypes = [...new Set(elementTypes)]
977+
const allLiterals = elementTypes.every(type =>
978+
/^-?\d+(\.\d+)?$/.test(type) || // numbers
979+
type === 'true' || type === 'false' || // booleans
980+
(type.startsWith('"') && type.endsWith('"')) || // strings
981+
(type.startsWith("'") && type.endsWith("'"))
982+
)
983+
984+
if (allLiterals && elementTypes.length <= 10) {
985+
// Create tuple for small arrays with literal types
986+
return `readonly [${elementTypes.join(', ')}]`
987+
}
984988

985989
if (uniqueTypes.length === 1) {
986990
return `Array<${uniqueTypes[0]}>`
@@ -1044,14 +1048,48 @@ function inferObjectType(value: string, isConst: boolean): string {
10441048
const properties = parseObjectProperties(content)
10451049
const propTypes: string[] = []
10461050

1047-
for (const [key, val] of properties) {
1048-
const valueType = inferNarrowType(val, isConst)
1051+
for (const [key, val] of properties) {
1052+
let valueType = inferNarrowType(val, isConst)
1053+
1054+
// Handle method signatures - clean up async and parameter defaults
1055+
if (valueType.includes('=>') || valueType.includes('function') || valueType.includes('async')) {
1056+
valueType = cleanMethodSignature(valueType)
1057+
}
1058+
10491059
propTypes.push(`${key}: ${valueType}`)
10501060
}
10511061

10521062
return `{\n ${propTypes.join(';\n ')}\n}`
10531063
}
10541064

1065+
/**
1066+
* Clean method signatures for declaration files
1067+
*/
1068+
function cleanMethodSignature(signature: string): string {
1069+
// Remove async modifier from method signatures (including in object methods)
1070+
let cleaned = signature.replace(/^async\s+/, '').replace(/\basync\s+/g, '')
1071+
1072+
// Remove parameter default values (e.g., currency = 'USD' becomes currency?)
1073+
cleaned = cleaned.replace(/(\w+)\s*=\s*[^,)]+/g, (match, paramName) => {
1074+
return `${paramName}?`
1075+
})
1076+
1077+
// Clean up extra spaces
1078+
cleaned = cleaned.replace(/\s+/g, ' ').trim()
1079+
1080+
return cleaned
1081+
}
1082+
1083+
/**
1084+
* Clean parameter defaults from function parameters
1085+
*/
1086+
function cleanParameterDefaults(params: string): string {
1087+
// Remove parameter default values and make them optional
1088+
return params.replace(/(\w+)\s*=\s*[^,)]+/g, (match, paramName) => {
1089+
return `${paramName}?`
1090+
})
1091+
}
1092+
10551093
/**
10561094
* Parse object properties
10571095
*/
@@ -1086,9 +1124,30 @@ function parseObjectProperties(content: string): Array<[string, string]> {
10861124
currentKey = current.trim()
10871125
current = ''
10881126
inKey = false
1127+
} else if (char === '(' && depth === 0 && inKey) {
1128+
// This might be a method definition like: methodName(params) or async methodName<T>(params)
1129+
currentKey = current.trim()
1130+
// Remove 'async' from the key if present
1131+
if (currentKey.startsWith('async ')) {
1132+
currentKey = currentKey.slice(6).trim()
1133+
}
1134+
current = char // Start with the opening parenthesis
1135+
inKey = false
1136+
depth = 1 // We're now inside the method definition
10891137
} else if (char === ',' && depth === 0) {
1090-
if (currentKey && current.trim()) {
1091-
properties.push([currentKey, current.trim()])
1138+
if (currentKey && current.trim()) {
1139+
// Clean method signatures before storing
1140+
let value = current.trim()
1141+
1142+
// Check if this is a method definition (starts with parentheses)
1143+
if (value.startsWith('(')) {
1144+
// This is a method definition like: (params): ReturnType { ... }
1145+
value = convertMethodToFunctionType(currentKey, value)
1146+
} else if (value.includes('=>') || value.includes('function') || value.includes('async')) {
1147+
value = cleanMethodSignature(value)
1148+
}
1149+
1150+
properties.push([currentKey, value])
10921151
}
10931152
current = ''
10941153
currentKey = ''
@@ -1101,14 +1160,64 @@ function parseObjectProperties(content: string): Array<[string, string]> {
11011160
}
11021161
}
11031162

1104-
// Don't forget the last property
1163+
// Don't forget the last property
11051164
if (currentKey && current.trim()) {
1106-
properties.push([currentKey, current.trim()])
1165+
let value = current.trim()
1166+
1167+
// Check if this is a method definition (starts with parentheses)
1168+
if (value.startsWith('(')) {
1169+
// This is a method definition like: (params): ReturnType { ... }
1170+
value = convertMethodToFunctionType(currentKey, value)
1171+
} else if (value.includes('=>') || value.includes('function') || value.includes('async')) {
1172+
value = cleanMethodSignature(value)
1173+
}
1174+
1175+
properties.push([currentKey, value])
11071176
}
11081177

11091178
return properties
11101179
}
11111180

1181+
/**
1182+
* Convert method definition to function type signature
1183+
*/
1184+
function convertMethodToFunctionType(methodName: string, methodDef: string): string {
1185+
// Remove async modifier if present
1186+
let cleaned = methodDef.replace(/^async\s+/, '')
1187+
1188+
// Extract generics, parameters, and return type
1189+
const genericMatch = cleaned.match(/^<([^>]+)>/)
1190+
const generics = genericMatch ? genericMatch[0] : ''
1191+
if (generics) {
1192+
cleaned = cleaned.slice(generics.length).trim()
1193+
}
1194+
1195+
// Find parameter list
1196+
const paramStart = cleaned.indexOf('(')
1197+
const paramEnd = findMatchingBracket(cleaned, paramStart, '(', ')')
1198+
1199+
if (paramStart === -1 || paramEnd === -1) {
1200+
return '() => unknown'
1201+
}
1202+
1203+
const params = cleaned.slice(paramStart, paramEnd + 1)
1204+
let returnType = 'unknown'
1205+
1206+
// Check for explicit return type annotation
1207+
const afterParams = cleaned.slice(paramEnd + 1).trim()
1208+
if (afterParams.startsWith(':')) {
1209+
const returnTypeMatch = afterParams.match(/^:\s*([^{]+)/)
1210+
if (returnTypeMatch) {
1211+
returnType = returnTypeMatch[1].trim()
1212+
}
1213+
}
1214+
1215+
// Clean parameter defaults
1216+
const cleanedParams = cleanParameterDefaults(params)
1217+
1218+
return `${generics}${cleanedParams} => ${returnType}`
1219+
}
1220+
11121221
/**
11131222
* Find matching bracket for nested structures
11141223
*/
@@ -1134,24 +1243,10 @@ function inferFunctionType(value: string, inUnion: boolean = false): string {
11341243
const trimmed = value.trim()
11351244

11361245
// Handle very complex function types early (but not function expressions)
1137-
if ((trimmed.length > 100 || (trimmed.match(/=>/g) || []).length > 2) && !trimmed.startsWith('function')) {
1138-
// Extract just the basic signature pattern
1139-
const genericMatch = trimmed.match(/^<[^>]+>/)
1140-
const generics = genericMatch ? genericMatch[0] : ''
1141-
1142-
// Look for first parameter pattern - need to find the complete parameter list
1143-
let paramStart = trimmed.indexOf('(')
1144-
if (paramStart !== -1) {
1145-
let paramEnd = findMatchingBracket(trimmed, paramStart, '(', ')')
1146-
if (paramEnd !== -1) {
1147-
const params = trimmed.substring(paramStart, paramEnd + 1)
1148-
const funcType = `${generics}${params} => any`
1149-
return inUnion ? `(${funcType})` : funcType
1150-
}
1151-
}
1152-
1153-
// Fallback if parameter extraction fails
1154-
const funcType = `${generics}(...args: any[]) => any`
1246+
// Only simplify if it's truly complex AND looks like a problematic signature
1247+
if (trimmed.length > 200 && (trimmed.match(/=>/g) || []).length > 2 && (trimmed.match(/</g) || []).length > 5 && !trimmed.startsWith('function')) {
1248+
// For extremely complex types, use a simple signature
1249+
const funcType = '(...args: any[]) => any'
11551250
return inUnion ? `(${funcType})` : funcType
11561251
}
11571252

@@ -1162,6 +1257,9 @@ function inferFunctionType(value: string, inUnion: boolean = false): string {
11621257
let params = asyncRemoved.substring(0, arrowIndex).trim()
11631258
let body = asyncRemoved.substring(arrowIndex + 2).trim()
11641259

1260+
// Clean up params - remove default values
1261+
params = cleanParameterDefaults(params)
1262+
11651263
// Clean up params
11661264
if (params === '()' || params === '') {
11671265
params = '()'
@@ -1218,6 +1316,9 @@ function inferFunctionType(value: string, inUnion: boolean = false): string {
12181316
params = params.substring(0, params.lastIndexOf('):')) + ')'
12191317
}
12201318

1319+
// Clean up params - remove default values
1320+
params = cleanParameterDefaults(params)
1321+
12211322
// Clean up params
12221323
if (params === '()' || params === '') {
12231324
params = '()'

test/fixtures/output/edge-cases.d.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ export declare function complexOverload(value: boolean): boolean;
44
export declare function complexOverload<T extends object>(value: T): T;
55
export declare function complexOverload(value: any): any;
66
export declare function asyncGenerator<T>(items: T[]): AsyncGenerator<T, void, unknown>;
7-
export declare const bigIntLiteral: bigint;
7+
export declare const bigIntLiteral: 123n;
88
export declare const bigIntExpression: bigint;
99
export declare const symbolUnique: symbol;
1010
export declare const symbolFor: symbol;
1111
export declare const templateSimple: `Hello World`;
12-
export declare const templateWithExpression: string;
12+
export declare const templateWithExpression: `Count: ${42}`;
1313
export declare const templateTagged: string;
14-
export declare const promiseResolved: Promise<number>;
14+
export declare const promiseResolved: Promise<42>;
1515
export declare const promiseRejected: Promise<never>;
16-
export declare const promiseAll: Promise<[number, 'two']>;
16+
export declare const promiseAll: Promise<[1, 'two']>;
1717
export declare const dateInstance: Date;
1818
export declare const mapInstance: Map<any, any>;
1919
export declare const setInstance: Set<any>;
@@ -29,9 +29,9 @@ export declare const deeplyNested: {
2929
}
3030
}
3131
};
32-
export declare const mixedTypeArray: Array<'string' | number | boolean | null | undefined | {
32+
export declare const mixedTypeArray: readonly ['string', 123, true, null, undefined, {
3333
key: 'value'
34-
} | Array<number> | (() => unknown) | Date | Promise<'async'>>;
34+
}, readonly [1, 2, 3], (() => unknown), Date, Promise<'async'>];
3535
export type ExtractPromise<T> = T extends Promise<infer U> ? U : never
3636
export type ExtractArray<T> = T extends (infer U)[] ? U : never
3737
export type Getters<T> = {

0 commit comments

Comments
 (0)