/// import ts from "typescript"; /** * Test file to explore TypeScript's type inference behavior in underspecified scenarios */ const code = ` // ============================================================================ // SCENARIO 1: Unused/unreferenced function parameters // ============================================================================ // No type annotation, parameter never used function unusedParam1(x) { return 42; } // No type annotation, parameter used but no operations that constrain type function unusedParam2(x) { console.log(x); return x; } // No type annotation, parameter used in generic operation function unusedParam3(x) { return { value: x }; } // Arrow function with unused parameter const unusedArrow1 = (x) => 42; // Arrow function that returns parameter const unusedArrow2 = (x) => x; // Multiple parameters, some unused function mixedUsage(x, y, z) { return y + 10; } // Callback parameter never annotated function takesCallback(cb) { return cb(5); } // ============================================================================ // SCENARIO 2: Object fields without annotations // ============================================================================ // Empty object literal const emptyObj = {}; // Object with unannotated field const objField1 = { x: undefined }; // Object field set to null const objField2 = { x: null }; // Object with no initializer in interface interface HasOptional { x?; } // Object spread with no type const spread1 = { ...{} }; // Destructured parameter with no type function destructure1({ x }) { return x; } // Destructured with default function destructure2({ x = 5 }) { return x; } // ============================================================================ // SCENARIO 3: Array operations // ============================================================================ // Empty array const emptyArr = []; // Array with no type annotation const arr1 = [1, 2, 3]; // Mixed array const mixedArr = [1, "hello", true]; // Array map with no annotation const mapped = [1, 2, 3].map(x => x * 2); // Array map where callback parameter type is ambiguous const mappedNoType = [1, 2, 3].map(x => ({ value: x })); // ============================================================================ // SCENARIO 4: Generic functions without constraints // ============================================================================ // Generic function with no constraints function identity(x: T): T { return x; } // Calling identity with no type argument const inferredIdentity = identity(42); // Generic with no constraint, parameter unused function genericUnused(x: T) { return "hello"; } // ============================================================================ // SCENARIO 5: Variable declarations // ============================================================================ // Variable with no initializer, no type annotation let uninitVar; // Variable initialized to undefined let explicitUndefined = undefined; // Variable from function with no return type function noReturn() { // no return statement } const fromNoReturn = noReturn(); // Variable from void function function voidFunc(): void { console.log("hi"); } const fromVoid = voidFunc(); // ============================================================================ // SCENARIO 6: Promise and async scenarios // ============================================================================ // Async function with no return type async function asyncNoReturn() { // no return } // Promise with no type argument const promise1 = new Promise((resolve) => { resolve(42); }); // Promise with resolve but no type const promise2 = new Promise((resolve, reject) => { resolve(); }); // ============================================================================ // SCENARIO 7: Class scenarios // ============================================================================ class NoFieldTypes { // Field with no type annotation field1; // Field initialized to undefined field2 = undefined; // Method parameter with no type method1(param) { return param; } // Constructor parameter with no type constructor(arg) { this.field1 = arg; } } // ============================================================================ // SCENARIO 8: Type assertions and casts // ============================================================================ // as any const asAny = 42 as any; // as unknown const asUnknown = 42 as unknown; // Double assertion const doubleAssert = 42 as unknown as string; // ============================================================================ // SCENARIO 9: Contextual typing scenarios // ============================================================================ // Event handler (contextual typing from DOM) // document.addEventListener("click", (e) => { // console.log(e); // }); // Array methods with contextual typing const filtered = [1, 2, 3].filter(x => x > 1); // Reduce with no type annotations const reduced = [1, 2, 3].reduce((acc, val) => acc + val); // Reduce with initial value of different type const reducedObj = [1, 2, 3].reduce((acc, val) => { acc[val] = val * 2; return acc; }, {}); // ============================================================================ // SCENARIO 10: Error handling // ============================================================================ try { throw new Error("test"); } catch (e) { // What is the type of 'e'? console.log(e); } // ============================================================================ // SCENARIO 11: Rest parameters and spread // ============================================================================ // Rest parameter with no type function restParams(...args) { return args; } // Destructuring rest function destructRest({ x, ...rest }) { return rest; } // ============================================================================ // SCENARIO 12: Index signatures // ============================================================================ // Object with index signature, no type const indexed = { [key: string]: undefined }; // Dynamic property access function getProp(obj, key) { return obj[key]; } // ============================================================================ // SCENARIO 13: Conditional types and inference // ============================================================================ // Type parameter inference in conditional type GetReturn = T extends (...args: any[]) => infer R ? R : never; // Using infer in a function function inferReturn any>(fn: T): GetReturn { return fn() as GetReturn; } // ============================================================================ // SCENARIO 14: Generators // ============================================================================ function* generatorNoType() { yield 1; yield 2; } function* generatorYieldUnknown() { yield; } `; // Create a TypeScript program to analyze these scenarios const fileName = "test.ts"; const sourceFile = ts.createSourceFile( fileName, code, ts.ScriptTarget.Latest, true, ); const options: ts.CompilerOptions = { target: ts.ScriptTarget.Latest, module: ts.ModuleKind.ESNext, strict: false, // Turn off strict mode to allow more inference noImplicitAny: false, // Allow implicit any }; const host = ts.createCompilerHost(options); host.getSourceFile = (name) => name === fileName ? sourceFile : undefined; host.writeFile = () => {}; host.getCurrentDirectory = () => ""; host.getCanonicalFileName = (name) => name; host.useCaseSensitiveFileNames = () => true; host.getNewLine = () => "\n"; const program = ts.createProgram([fileName], options, host); const checker = program.getTypeChecker(); // Helper to get type information function analyzeNode(node: ts.Node, name: string) { const type = checker.getTypeAtLocation(node); const typeString = checker.typeToString(type); const flags = type.flags; const flagNames: string[] = []; if (flags & ts.TypeFlags.Any) flagNames.push("Any"); if (flags & ts.TypeFlags.Unknown) flagNames.push("Unknown"); if (flags & ts.TypeFlags.String) flagNames.push("String"); if (flags & ts.TypeFlags.Number) flagNames.push("Number"); if (flags & ts.TypeFlags.Boolean) flagNames.push("Boolean"); if (flags & ts.TypeFlags.Void) flagNames.push("Void"); if (flags & ts.TypeFlags.Undefined) flagNames.push("Undefined"); if (flags & ts.TypeFlags.Null) flagNames.push("Null"); if (flags & ts.TypeFlags.Never) flagNames.push("Never"); return { name, typeString, flags: flagNames.join(" | ") || `Other (${flags})`, }; } console.log("=".repeat(80)); console.log("TypeScript Type Inference Exploration"); console.log("=".repeat(80)); console.log(); const results: ReturnType[] = []; // Visit all variable declarations and function parameters function visit(node: ts.Node) { // Function parameters if ( ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) ) { const funcName = ts.isFunctionDeclaration(node) && node.name ? node.name.text : ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name) ? node.parent.name.text : "anonymous"; node.parameters.forEach((param, idx) => { if (ts.isIdentifier(param.name)) { results.push( analyzeNode(param, `${funcName}::param[${idx}] '${param.name.text}'`), ); } }); } // Variable declarations if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) { results.push(analyzeNode(node, `var '${node.name.text}'`)); } // Class fields if (ts.isPropertyDeclaration(node) && ts.isIdentifier(node.name)) { const className = node.parent && ts.isClassDeclaration(node.parent) && node.parent.name ? node.parent.name.text : "anonymous"; results.push(analyzeNode(node, `${className}.${node.name.text}`)); } // Catch clause variables if (ts.isCatchClause(node) && node.variableDeclaration) { const varDecl = node.variableDeclaration; if (ts.isIdentifier(varDecl.name)) { results.push(analyzeNode(varDecl, `catch '${varDecl.name.text}'`)); } } ts.forEachChild(node, visit); } visit(sourceFile); // Group results by category const scenarios = { "Function Parameters (unused/unreferenced)": results.filter((r) => r.name.includes("::param") ), "Variables": results.filter((r) => r.name.startsWith("var")), "Class Fields": results.filter((r) => r.name.includes(".")), "Error Handling": results.filter((r) => r.name.startsWith("catch")), }; for (const [category, items] of Object.entries(scenarios)) { if (items.length === 0) continue; console.log(`\n${"=".repeat(80)}`); console.log(category); console.log("=".repeat(80)); for (const item of items) { console.log(`\n${item.name}`); console.log(` Type: ${item.typeString}`); console.log(` Flags: ${item.flags}`); } } // Summary statistics console.log(`\n${"=".repeat(80)}`); console.log("Summary Statistics"); console.log("=".repeat(80)); const anyCount = results.filter((r) => r.flags.includes("Any")).length; const unknownCount = results.filter((r) => r.flags.includes("Unknown")).length; const undefinedCount = results.filter((r) => r.flags.includes("Undefined")).length; const voidCount = results.filter((r) => r.flags.includes("Void")).length; const neverCount = results.filter((r) => r.flags.includes("Never")).length; console.log(`\nTotal items analyzed: ${results.length}`); console.log( ` 'any' type: ${anyCount} (${ (anyCount / results.length * 100).toFixed(1) }%)`, ); console.log( ` 'unknown' type: ${unknownCount} (${ (unknownCount / results.length * 100).toFixed(1) }%)`, ); console.log( ` 'undefined' type: ${undefinedCount} (${ (undefinedCount / results.length * 100).toFixed(1) }%)`, ); console.log( ` 'void' type: ${voidCount} (${ (voidCount / results.length * 100).toFixed(1) }%)`, ); console.log( ` 'never' type: ${neverCount} (${ (neverCount / results.length * 100).toFixed(1) }%)`, );