Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion: Parameter type inference from function body #17715

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4298,7 +4298,7 @@ namespace ts {
type = getContextualThisParameterType(func);
}
else {
type = getContextuallyTypedParameterType(<ParameterDeclaration>declaration);
type = getContextuallyTypedParameterType(<ParameterDeclaration>declaration)
}
if (type) {
return addOptionality(type, /*optional*/ declaration.questionToken && includeOptionality);
Expand Down Expand Up @@ -4327,6 +4327,12 @@ namespace ts {
return getTypeFromBindingPattern(<BindingPattern>declaration.name, /*includePatternInType*/ false, /*reportErrors*/ true);
}

// Important to do this *after* attempt has been made to resolve via initializer
if (declaration.kind === SyntaxKind.Parameter) {
const inferredType = getParameterTypeFromBody(<ParameterDeclaration>declaration)
if (inferredType) return inferredType
}

// No type specified and nothing can be inferred
return undefined;
}
Expand Down Expand Up @@ -12798,6 +12804,20 @@ namespace ts {
return undefined;
}

function getParameterTypeFromBody(parameter: ParameterDeclaration): Type {
const func = <FunctionLikeDeclaration>parameter.parent
if (!func.body) {
return unknownType;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getIntersectionType cannot handle flow sensitive typing. For example,

function test(a) {
  if (someCond) roundNumber(a)
  else trimString(a)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HerringtonDarkholme Could you provide a concrete example of this? For which reference is someCond affecting the type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HerringtonDarkholme I understand what you mean now; in the case you highlighted a should be inferred as number | string instead of number & string. number & string is an excessively conservative inference, but it is a sound one: number & string is assignable to number | string.

Some options are to bail and infer any for parameters used in branched code, keep the existing behavior and expect the user to manually supply typings when they realize number & string is unsatisfiable, or to actually analyze the flow graph and generate the appropriate union type.

let type: Type;
let types: Type[];
types = checkAndAggregateParameterExpressionTypes(parameter)
type = types ? getWidenedType(getIntersectionType(types)) : undefined;

return type;
}

// Return contextual type of parameter or undefined if no contextual type is available
function getContextuallyTypedParameterType(parameter: ParameterDeclaration): Type {
const func = parameter.parent;
Expand Down Expand Up @@ -16729,6 +16749,24 @@ namespace ts {
return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clearly, only collecting invocation isn't enough. But I wonder how other expressions can be handled. For example, how arg.assigment = 123 is handled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HerringtonDarkholme I've put out some initial thoughts in #15114 (comment). We can exploit co and contravariance to get sensible rules here. If something assigns to a parameter, the sensible thing is for the parameter to be inferred as having a supertype of said assignment. Unfortunately TypeScript can't represent this.

This means we should restrict ourselves to inference in covariant usages of the parameter variable, i.e. wherever it is used as a source of information (assignment of the parameter to well-typed reference, invocation of the parameter with well-typed argument list, accessing properties on parameter, etc.)

}

function checkAndAggregateParameterExpressionTypes(parameter: ParameterDeclaration): Type[] {
const func = <FunctionLikeDeclaration>parameter.parent
const usageTypes: Type[] = []
forEachInvocation(<Block>func.body, invocation => {
const usages = invocation.arguments
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this work with recursive function? I guess it need some guard statement.

Copy link
Contributor Author

@masaeedu masaeedu Aug 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getTypeOfParameter calls getTypeOfSymbol, which internally has a stack checking for circularity and ejecting with "unknown". I believe we can rely on this, but I need to add tests.

.map((arg, i) => ({ arg, symbol: getSymbolAtLocation(arg), i }))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the parameterType is a generic type parameter? Should it be propagated to the inferred function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, probably it should be ensured that the function currently under inference does not already have an explicit type parameter list, and a type parameter to add polymorphism wrt the parameter under inference. If there is already a type parameter list we should just bail.

.filter(({ symbol }) => symbol && symbol.valueDeclaration === parameter)
if (!usages.length) return
const funcSymbol = getSymbolAtLocation(invocation.expression)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to find robust mechanism for resolving the node corresponding to the invoked function to a function signature. Right now things like interface Foo{ (bar: Bar): Baz } can mess it up.

if (!funcSymbol) return
const sig = getSignatureFromDeclaration(funcSymbol.valueDeclaration as FunctionLikeDeclaration)
const parameterTypes = sig.parameters.map(getTypeOfParameter)
const argumentTypes = usages.map(({ i }) => parameterTypes[i])
usageTypes.splice(0, 0, ...argumentTypes);
});
return usageTypes.length ? usageTypes : undefined;
}

function checkAndAggregateReturnExpressionTypes(func: FunctionLikeDeclaration, checkMode: CheckMode): Type[] {
const functionFlags = getFunctionFlags(func);
const aggregatedTypes: Type[] = [];
Expand Down
14 changes: 14 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,20 @@ namespace ts {
return false;
}

export function forEachInvocation<T>(body: Block, visitor: (stmt: CallExpression) => T): T {

return traverse(body);

function traverse(node: Node): T {
switch (node.kind) {
case SyntaxKind.CallExpression:
return visitor(<CallExpression>node)
default:
return forEachChild(node, traverse);
}
}
}

// Warning: This has the same semantics as the forEach family of functions,
// in that traversal terminates in the event that 'visitor' supplies a truthy value.
export function forEachReturnStatement<T>(body: Block, visitor: (stmt: ReturnStatement) => T): T {
Expand Down
22 changes: 22 additions & 0 deletions tests/cases/compiler/parameterInference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// CASE 1
function foo(s) {
Math.sqrt(s)
}

// CASE 2
declare function swapNumberString(n: string): number;
declare function swapNumberString(n: number): string;

// Should have identical signature to swapNumberString
function subs(s) {
return swapNumberString(s);
}

// CASE 3
function f(x: number){
return x;
}

function g(x){ return x};

function h(x){ return f(x); };