-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Changes from all commits
a602e1f
99b5362
adfd904
4d8a8b3
d14c511
db39b43
6095dfd
5bc6895
9bec8c1
9905af7
a5d6542
7672747
351f665
ef5f7bc
15b6c0c
d85a212
cd788ee
f2853ae
aff19fc
818b9c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
} | ||
|
@@ -12798,6 +12804,14 @@ namespace ts { | |
return undefined; | ||
} | ||
|
||
function getParameterTypeFromBody(parameter: ParameterDeclaration): Type { | ||
const func = <FunctionLikeDeclaration>parameter.parent; | ||
if (!func.body || isRestParameter(parameter)) return; | ||
|
||
const types = checkAndAggregateParameterExpressionTypes(parameter); | ||
return types ? getWidenedType(getIntersectionType(types)) : undefined; | ||
} | ||
|
||
// Return contextual type of parameter or undefined if no contextual type is available | ||
function getContextuallyTypedParameterType(parameter: ParameterDeclaration): Type { | ||
const func = parameter.parent; | ||
|
@@ -16729,6 +16743,36 @@ namespace ts { | |
return true; | ||
} | ||
|
||
function checkAndAggregateParameterExpressionTypes(parameter: ParameterDeclaration): Type[] { | ||
const func = <FunctionLikeDeclaration>parameter.parent; | ||
const usageTypes: Type[] = []; | ||
const invocations: CallExpression[] = []; | ||
|
||
function seekInvocations(f: FunctionBody) { | ||
forEachInvocation(f, invocation => { | ||
invocations.push(invocation); | ||
invocation.arguments.filter(isFunctionExpressionOrArrowFunction).forEach(arg => seekInvocations(<Block>arg.body)); | ||
}); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
seekInvocations(<Block>func.body); | ||
|
||
invocations.forEach(invocation => { | ||
const usages = invocation.arguments | ||
.map((arg, i) => ({ arg, symbol: getSymbolAtLocation(arg), i })) | ||
.filter(({ symbol }) => symbol && symbol.valueDeclaration === parameter); | ||
if (!usages.length) return; | ||
const funcSymbol = getSymbolAtLocation(invocation.expression); | ||
if (!funcSymbol || !isFunctionLike(funcSymbol.valueDeclaration)) return; | ||
const sig = getSignatureFromDeclaration(funcSymbol.valueDeclaration); | ||
const parameterTypes = sig.parameters.map(getTypeOfParameter); | ||
const argumentTypes = usages.map(({ i }) => parameterTypes[i]).filter(t => !!t); | ||
usageTypes.splice(0, 0, ...argumentTypes); | ||
}); | ||
|
||
return usageTypes.length ? usageTypes : undefined; | ||
} | ||
|
||
function checkAndAggregateReturnExpressionTypes(func: FunctionLikeDeclaration, checkMode: CheckMode): Type[] { | ||
const functionFlags = getFunctionFlags(func); | ||
const aggregatedTypes: Type[] = []; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
//// [parameterInference.ts] | ||
// CASE 1 | ||
function foo(s) { | ||
Math.sqrt(s) | ||
} | ||
// => function foo(s: number): void | ||
|
||
// CASE 2 | ||
declare function swapNumberString(n: string): number; | ||
declare function swapNumberString(n: number): string; | ||
|
||
function subs(s) { | ||
return swapNumberString(s); | ||
} | ||
// => function subs(s: string): number | ||
// NOTE: Still broken, needs to deal with overloads. Should have been inferred as: | ||
// => (s: string) => number & (s: number) => string | ||
|
||
// CASE 3 | ||
function f3(x: number){ | ||
return x; | ||
} | ||
|
||
function g3(x){ return f3(x); }; | ||
// => function g3(x: number): number | ||
|
||
// CASE 4 | ||
declare function f4(g: Function) | ||
function g4(x) { | ||
f4(() => { | ||
Math.sqrt(x) | ||
}) | ||
} | ||
|
||
|
||
//// [parameterInference.js] | ||
// CASE 1 | ||
function foo(s) { | ||
Math.sqrt(s); | ||
} | ||
function subs(s) { | ||
return swapNumberString(s); | ||
} | ||
// => function subs(s: string): number | ||
// NOTE: Still broken, needs to deal with overloads. Should have been inferred as: | ||
// => (s: string) => number & (s: number) => string | ||
// CASE 3 | ||
function f3(x) { | ||
return x; | ||
} | ||
function g3(x) { return f3(x); } | ||
; | ||
function g4(x) { | ||
f4(function () { | ||
Math.sqrt(x); | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
=== tests/cases/compiler/parameterInference.ts === | ||
// CASE 1 | ||
function foo(s) { | ||
>foo : Symbol(foo, Decl(parameterInference.ts, 0, 0)) | ||
>s : Symbol(s, Decl(parameterInference.ts, 1, 13)) | ||
|
||
Math.sqrt(s) | ||
>Math.sqrt : Symbol(Math.sqrt, Decl(lib.d.ts, --, --)) | ||
>Math : Symbol(Math, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) | ||
>sqrt : Symbol(Math.sqrt, Decl(lib.d.ts, --, --)) | ||
>s : Symbol(s, Decl(parameterInference.ts, 1, 13)) | ||
} | ||
// => function foo(s: number): void | ||
|
||
// CASE 2 | ||
declare function swapNumberString(n: string): number; | ||
>swapNumberString : Symbol(swapNumberString, Decl(parameterInference.ts, 3, 1), Decl(parameterInference.ts, 7, 53)) | ||
>n : Symbol(n, Decl(parameterInference.ts, 7, 34)) | ||
|
||
declare function swapNumberString(n: number): string; | ||
>swapNumberString : Symbol(swapNumberString, Decl(parameterInference.ts, 3, 1), Decl(parameterInference.ts, 7, 53)) | ||
>n : Symbol(n, Decl(parameterInference.ts, 8, 34)) | ||
|
||
function subs(s) { | ||
>subs : Symbol(subs, Decl(parameterInference.ts, 8, 53)) | ||
>s : Symbol(s, Decl(parameterInference.ts, 10, 14)) | ||
|
||
return swapNumberString(s); | ||
>swapNumberString : Symbol(swapNumberString, Decl(parameterInference.ts, 3, 1), Decl(parameterInference.ts, 7, 53)) | ||
>s : Symbol(s, Decl(parameterInference.ts, 10, 14)) | ||
} | ||
// => function subs(s: string): number | ||
// NOTE: Still broken, needs to deal with overloads. Should have been inferred as: | ||
// => (s: string) => number & (s: number) => string | ||
|
||
// CASE 3 | ||
function f3(x: number){ | ||
>f3 : Symbol(f3, Decl(parameterInference.ts, 12, 1)) | ||
>x : Symbol(x, Decl(parameterInference.ts, 18, 12)) | ||
|
||
return x; | ||
>x : Symbol(x, Decl(parameterInference.ts, 18, 12)) | ||
} | ||
|
||
function g3(x){ return f3(x); }; | ||
>g3 : Symbol(g3, Decl(parameterInference.ts, 20, 1)) | ||
>x : Symbol(x, Decl(parameterInference.ts, 22, 12)) | ||
>f3 : Symbol(f3, Decl(parameterInference.ts, 12, 1)) | ||
>x : Symbol(x, Decl(parameterInference.ts, 22, 12)) | ||
|
||
// => function g3(x: number): number | ||
|
||
// CASE 4 | ||
declare function f4(g: Function) | ||
>f4 : Symbol(f4, Decl(parameterInference.ts, 22, 32)) | ||
>g : Symbol(g, Decl(parameterInference.ts, 26, 20)) | ||
>Function : Symbol(Function, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) | ||
|
||
function g4(x) { | ||
>g4 : Symbol(g4, Decl(parameterInference.ts, 26, 32)) | ||
>x : Symbol(x, Decl(parameterInference.ts, 27, 12)) | ||
|
||
f4(() => { | ||
>f4 : Symbol(f4, Decl(parameterInference.ts, 22, 32)) | ||
|
||
Math.sqrt(x) | ||
>Math.sqrt : Symbol(Math.sqrt, Decl(lib.d.ts, --, --)) | ||
>Math : Symbol(Math, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) | ||
>sqrt : Symbol(Math.sqrt, Decl(lib.d.ts, --, --)) | ||
>x : Symbol(x, Decl(parameterInference.ts, 27, 12)) | ||
|
||
}) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
=== tests/cases/compiler/parameterInference.ts === | ||
// CASE 1 | ||
function foo(s) { | ||
>foo : (s: number) => void | ||
>s : number | ||
|
||
Math.sqrt(s) | ||
>Math.sqrt(s) : number | ||
>Math.sqrt : (x: number) => number | ||
>Math : Math | ||
>sqrt : (x: number) => number | ||
>s : number | ||
} | ||
// => function foo(s: number): void | ||
|
||
// CASE 2 | ||
declare function swapNumberString(n: string): number; | ||
>swapNumberString : { (n: string): number; (n: number): string; } | ||
>n : string | ||
|
||
declare function swapNumberString(n: number): string; | ||
>swapNumberString : { (n: string): number; (n: number): string; } | ||
>n : number | ||
|
||
function subs(s) { | ||
>subs : (s: string) => number | ||
>s : string | ||
|
||
return swapNumberString(s); | ||
>swapNumberString(s) : number | ||
>swapNumberString : { (n: string): number; (n: number): string; } | ||
>s : string | ||
} | ||
// => function subs(s: string): number | ||
// NOTE: Still broken, needs to deal with overloads. Should have been inferred as: | ||
// => (s: string) => number & (s: number) => string | ||
|
||
// CASE 3 | ||
function f3(x: number){ | ||
>f3 : (x: number) => number | ||
>x : number | ||
|
||
return x; | ||
>x : number | ||
} | ||
|
||
function g3(x){ return f3(x); }; | ||
>g3 : (x: number) => number | ||
>x : number | ||
>f3(x) : number | ||
>f3 : (x: number) => number | ||
>x : number | ||
|
||
// => function g3(x: number): number | ||
|
||
// CASE 4 | ||
declare function f4(g: Function) | ||
>f4 : (g: Function) => any | ||
>g : Function | ||
>Function : Function | ||
|
||
function g4(x) { | ||
>g4 : (x: number) => void | ||
>x : number | ||
|
||
f4(() => { | ||
>f4(() => { Math.sqrt(x) }) : any | ||
>f4 : (g: Function) => any | ||
>() => { Math.sqrt(x) } : () => void | ||
|
||
Math.sqrt(x) | ||
>Math.sqrt(x) : number | ||
>Math.sqrt : (x: number) => number | ||
>Math : Math | ||
>sqrt : (x: number) => number | ||
>x : number | ||
|
||
}) | ||
} | ||
|
There was a problem hiding this comment.
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,
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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 asnumber | string
instead ofnumber & string
.number & string
is an excessively conservative inference, but it is a sound one:number & string
is assignable tonumber | 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 realizenumber & string
is unsatisfiable, or to actually analyze the flow graph and generate the appropriate union type.