-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Infer from context sensitive return expressions #60909
base: main
Are you sure you want to change the base?
Changes from all commits
65b54ad
7ccc000
37af847
4aa5ca7
3095dad
c671b45
c61ea39
23c6c33
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 |
---|---|---|
|
@@ -1329,11 +1329,14 @@ export const enum CheckMode { | |
Inferential = 1 << 1, // Inferential typing | ||
SkipContextSensitive = 1 << 2, // Skip context sensitive function expressions | ||
SkipGenericFunctions = 1 << 3, // Skip single signature generic functions | ||
IsForSignatureHelp = 1 << 4, // Call resolution for purposes of signature help | ||
RestBindingElement = 1 << 5, // Checking a type that is going to be used to determine the type of a rest binding element | ||
SkipReturnTypeFromBodyInference = 1 << 4, // Skip inferring from return types of context sensitive functions | ||
// it's used to prevent inferring within return types of generic functions, | ||
// as that could create overlapping inferences that would interfere with the logic `instantiateTypeWithSingleGenericCallSignature` that handles them better | ||
IsForSignatureHelp = 1 << 5, // Call resolution for purposes of signature help | ||
RestBindingElement = 1 << 6, // Checking a type that is going to be used to determine the type of a rest binding element | ||
// e.g. in `const { a, ...rest } = foo`, when checking the type of `foo` to determine the type of `rest`, | ||
// we need to preserve generic types instead of substituting them for constraints | ||
TypeOnly = 1 << 6, // Called from getTypeOfExpression, diagnostics may be omitted | ||
TypeOnly = 1 << 7, // Called from getTypeOfExpression, diagnostics may be omitted | ||
} | ||
|
||
/** @internal */ | ||
|
@@ -38938,7 +38941,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
return getTypeOfSymbol(getSymbolOfDeclaration(node)); | ||
} | ||
|
||
function contextuallyCheckFunctionExpressionOrObjectLiteralMethod(node: FunctionExpression | ArrowFunction | MethodDeclaration, checkMode?: CheckMode) { | ||
function contextuallyCheckFunctionExpressionOrObjectLiteralMethod(node: FunctionExpression | ArrowFunction | MethodDeclaration, checkMode = CheckMode.Normal) { | ||
const links = getNodeLinks(node); | ||
// Check if function expression is contextually typed and assign parameter types if so. | ||
if (!(links.flags & NodeCheckFlags.ContextChecked)) { | ||
|
@@ -38952,11 +38955,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
if (!signature) { | ||
return; | ||
} | ||
if (isContextSensitive(node)) { | ||
const isNodeContextSensitive = isContextSensitive(node); | ||
if (isNodeContextSensitive) { | ||
if (contextualSignature) { | ||
const inferenceContext = getInferenceContext(node); | ||
let instantiatedContextualSignature: Signature | undefined; | ||
if (checkMode && checkMode & CheckMode.Inferential) { | ||
if (checkMode & CheckMode.Inferential) { | ||
inferFromAnnotatedParameters(signature, contextualSignature, inferenceContext!); | ||
const restType = getEffectiveRestType(contextualSignature); | ||
if (restType && restType.flags & TypeFlags.TypeParameter) { | ||
|
@@ -38974,15 +38978,34 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
} | ||
else if (contextualSignature && !node.typeParameters && contextualSignature.parameters.length > node.parameters.length) { | ||
const inferenceContext = getInferenceContext(node); | ||
if (checkMode && checkMode & CheckMode.Inferential) { | ||
if (checkMode & CheckMode.Inferential) { | ||
inferFromAnnotatedParameters(signature, contextualSignature, inferenceContext!); | ||
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. I literally wrote this code in August so I need to take another look at this, self-review it and add more tests. In the meantime though, it could be helpful for me to learn what the extended test suite thinks about this. cc @jakebailey :p 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. Hm, damn self-check :p I need to fix this first: repro |
||
} | ||
} | ||
if (contextualSignature && !getReturnTypeFromAnnotation(node) && !signature.resolvedReturnType) { | ||
const returnType = getReturnTypeFromBody(node, checkMode); | ||
if (!signature.resolvedReturnType) { | ||
signature.resolvedReturnType = returnType; | ||
let contextualReturnType: Type; | ||
let returnType: Type; | ||
|
||
if (node.typeParameters) { | ||
checkMode |= CheckMode.SkipReturnTypeFromBodyInference; | ||
} | ||
|
||
if ( | ||
isNodeContextSensitive && ((checkMode & (CheckMode.Inferential | CheckMode.SkipReturnTypeFromBodyInference)) === CheckMode.Inferential) && | ||
couldContainTypeVariables(contextualReturnType = getReturnTypeOfSignature(contextualSignature)) | ||
) { | ||
const inferenceContext = getInferenceContext(node); | ||
const isReturnContextSensitive = !!node.body && (node.body.kind === SyntaxKind.Block ? forEachReturnStatement(node.body as Block, statement => !!statement.expression && isContextSensitive(statement.expression)) : isContextSensitive(node.body)); | ||
returnType = getReturnTypeFromBody(node, checkMode | (isReturnContextSensitive ? CheckMode.SkipContextSensitive : 0)); | ||
inferTypes(inferenceContext!.inferences, returnType, contextualReturnType); | ||
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. alternatively, this could be handled by intra expression inference but that is order-sensitive and traditionally simple cases like this one aren't: declare function test<T>(_: {
stuff: T,
consume: (arg: T) => void
}): void
test({
consume: (arg) => {},
stuff: 'foo' // this can come after `consume`
}) So the reason I put this logic here is that it allows for the same order-independence |
||
if (isReturnContextSensitive) { | ||
returnType = getReturnTypeFromBody(node, checkMode); | ||
} | ||
} | ||
else { | ||
returnType = getReturnTypeFromBody(node, checkMode); | ||
} | ||
signature.resolvedReturnType ??= returnType; | ||
} | ||
checkSignatureDeclaration(node); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,295 @@ | ||
//// [tests/cases/conformance/types/typeRelationships/typeInference/InferFromReturnsInContextSensitive1.ts] //// | ||
|
||
//// [InferFromReturnsInContextSensitive1.ts] | ||
// https://github.com/microsoft/TypeScript/issues/60720 | ||
|
||
type Options<TContext> = { | ||
onStart?: () => TContext; | ||
onEnd?: (context: TContext) => void; | ||
}; | ||
|
||
function create<TContext>(builder: (arg: boolean) => Options<TContext>) { | ||
return builder(true); | ||
} | ||
|
||
create((arg) => ({ | ||
onStart: () => ({ time: new Date() }), | ||
onEnd: (context) => {}, | ||
})); | ||
|
||
create((arg) => ({ | ||
onEnd: (context) => {}, | ||
onStart: () => ({ time: new Date() }), | ||
})); | ||
|
||
// https://github.com/microsoft/TypeScript/issues/57021 | ||
|
||
type Schema = Record<string, unknown>; | ||
|
||
type StepFunction<TSchema extends Schema = Schema> = (anything: unknown) => { | ||
readonly schema: TSchema; | ||
readonly toAnswers?: (keys: keyof TSchema) => unknown; | ||
}; | ||
|
||
function step<TSchema extends Schema = Schema>( | ||
stepVal: StepFunction<TSchema>, | ||
): StepFunction<TSchema> { | ||
return stepVal; | ||
} | ||
|
||
const stepResult1 = step((_something) => ({ | ||
schema: { | ||
attribute: "anything", | ||
}, | ||
toAnswers: (keys) => { | ||
type Test = string extends typeof keys ? never : "true"; | ||
const test: Test = "true"; // ok | ||
return { test }; | ||
}, | ||
})); | ||
|
||
const stepResult2 = step((_something) => ({ | ||
toAnswers: (keys) => { | ||
type Test = string extends typeof keys ? never : "true"; | ||
const test: Test = "true"; // ok | ||
return { test }; | ||
}, | ||
schema: { | ||
attribute: "anything", | ||
}, | ||
})); | ||
|
||
type Fn1<T, T2> = (anything: unknown) => { | ||
stuff: T; | ||
consume: (arg: T) => (anything: unknown) => { | ||
stuff2: T2; | ||
consume2: (arg: T2) => void; | ||
}; | ||
}; | ||
|
||
declare function test1<T, T2>(fn: Fn1<T, T2>): [T, T2]; | ||
|
||
const res1 = test1((_something) => ({ | ||
stuff: "foo", | ||
consume: (arg) => { | ||
return (_something) => ({ | ||
stuff2: 42, | ||
consume2: (arg2) => {}, | ||
}); | ||
}, | ||
})); | ||
|
||
const res2 = test1((_something) => ({ | ||
consume: (arg) => { | ||
return (_something) => ({ | ||
consume2: (arg2) => {}, | ||
stuff2: 42, | ||
}); | ||
}, | ||
stuff: "foo", | ||
})); | ||
|
||
const res3 = test1((_something) => ({ | ||
stuff: "foo", | ||
consume: () => { | ||
return (_something) => ({ | ||
stuff2: 42, | ||
consume2: (arg2) => {}, | ||
}); | ||
}, | ||
})); | ||
|
||
const res4 = test1((_something) => ({ | ||
consume: () => { | ||
return (_something) => ({ | ||
consume2: (arg2) => {}, | ||
stuff2: 42, | ||
}); | ||
}, | ||
stuff: "foo", | ||
})); | ||
|
||
const res5 = test1((_something) => ({ | ||
stuff: "foo", | ||
consume: () => { | ||
return () => ({ | ||
stuff2: 42, | ||
consume2: (arg2) => {}, | ||
}); | ||
}, | ||
})); | ||
|
||
const res6 = test1((_something) => ({ | ||
consume: () => { | ||
return () => ({ | ||
consume2: (arg2) => {}, | ||
stuff2: 42, | ||
}); | ||
}, | ||
stuff: "foo", | ||
})); | ||
|
||
const res7 = test1((_something) => ({ | ||
stuff: "foo", | ||
consume: () => { | ||
return () => ({ | ||
stuff2: 42, | ||
consume2: () => {}, | ||
}); | ||
}, | ||
})); | ||
|
||
const res8 = test1((_something) => ({ | ||
consume: () => { | ||
return () => ({ | ||
consume2: () => {}, | ||
stuff2: 42, | ||
}); | ||
}, | ||
stuff: "foo", | ||
})); | ||
|
||
|
||
//// [InferFromReturnsInContextSensitive1.js] | ||
"use strict"; | ||
// https://github.com/microsoft/TypeScript/issues/60720 | ||
function create(builder) { | ||
return builder(true); | ||
} | ||
create(function (arg) { return ({ | ||
onStart: function () { return ({ time: new Date() }); }, | ||
onEnd: function (context) { }, | ||
}); }); | ||
create(function (arg) { return ({ | ||
onEnd: function (context) { }, | ||
onStart: function () { return ({ time: new Date() }); }, | ||
}); }); | ||
function step(stepVal) { | ||
return stepVal; | ||
} | ||
var stepResult1 = step(function (_something) { return ({ | ||
schema: { | ||
attribute: "anything", | ||
}, | ||
toAnswers: function (keys) { | ||
var test = "true"; // ok | ||
return { test: test }; | ||
}, | ||
}); }); | ||
var stepResult2 = step(function (_something) { return ({ | ||
toAnswers: function (keys) { | ||
var test = "true"; // ok | ||
return { test: test }; | ||
}, | ||
schema: { | ||
attribute: "anything", | ||
}, | ||
}); }); | ||
var res1 = test1(function (_something) { return ({ | ||
stuff: "foo", | ||
consume: function (arg) { | ||
return function (_something) { return ({ | ||
stuff2: 42, | ||
consume2: function (arg2) { }, | ||
}); }; | ||
}, | ||
}); }); | ||
var res2 = test1(function (_something) { return ({ | ||
consume: function (arg) { | ||
return function (_something) { return ({ | ||
consume2: function (arg2) { }, | ||
stuff2: 42, | ||
}); }; | ||
}, | ||
stuff: "foo", | ||
}); }); | ||
var res3 = test1(function (_something) { return ({ | ||
stuff: "foo", | ||
consume: function () { | ||
return function (_something) { return ({ | ||
stuff2: 42, | ||
consume2: function (arg2) { }, | ||
}); }; | ||
}, | ||
}); }); | ||
var res4 = test1(function (_something) { return ({ | ||
consume: function () { | ||
return function (_something) { return ({ | ||
consume2: function (arg2) { }, | ||
stuff2: 42, | ||
}); }; | ||
}, | ||
stuff: "foo", | ||
}); }); | ||
var res5 = test1(function (_something) { return ({ | ||
stuff: "foo", | ||
consume: function () { | ||
return function () { return ({ | ||
stuff2: 42, | ||
consume2: function (arg2) { }, | ||
}); }; | ||
}, | ||
}); }); | ||
var res6 = test1(function (_something) { return ({ | ||
consume: function () { | ||
return function () { return ({ | ||
consume2: function (arg2) { }, | ||
stuff2: 42, | ||
}); }; | ||
}, | ||
stuff: "foo", | ||
}); }); | ||
var res7 = test1(function (_something) { return ({ | ||
stuff: "foo", | ||
consume: function () { | ||
return function () { return ({ | ||
stuff2: 42, | ||
consume2: function () { }, | ||
}); }; | ||
}, | ||
}); }); | ||
var res8 = test1(function (_something) { return ({ | ||
consume: function () { | ||
return function () { return ({ | ||
consume2: function () { }, | ||
stuff2: 42, | ||
}); }; | ||
}, | ||
stuff: "foo", | ||
}); }); | ||
|
||
|
||
//// [InferFromReturnsInContextSensitive1.d.ts] | ||
type Options<TContext> = { | ||
onStart?: () => TContext; | ||
onEnd?: (context: TContext) => void; | ||
}; | ||
declare function create<TContext>(builder: (arg: boolean) => Options<TContext>): Options<TContext>; | ||
type Schema = Record<string, unknown>; | ||
type StepFunction<TSchema extends Schema = Schema> = (anything: unknown) => { | ||
readonly schema: TSchema; | ||
readonly toAnswers?: (keys: keyof TSchema) => unknown; | ||
}; | ||
declare function step<TSchema extends Schema = Schema>(stepVal: StepFunction<TSchema>): StepFunction<TSchema>; | ||
declare const stepResult1: StepFunction<{ | ||
attribute: string; | ||
}>; | ||
declare const stepResult2: StepFunction<{ | ||
attribute: string; | ||
}>; | ||
type Fn1<T, T2> = (anything: unknown) => { | ||
stuff: T; | ||
consume: (arg: T) => (anything: unknown) => { | ||
stuff2: T2; | ||
consume2: (arg: T2) => void; | ||
}; | ||
}; | ||
declare function test1<T, T2>(fn: Fn1<T, T2>): [T, T2]; | ||
declare const res1: [string, number]; | ||
declare const res2: [string, number]; | ||
declare const res3: [string, number]; | ||
declare const res4: [string, number]; | ||
declare const res5: [string, number]; | ||
declare const res6: [string, number]; | ||
declare const res7: [string, number]; | ||
declare const res8: [string, 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.
It's separate from
SkipGenericFunctions
becausegetReturnTypeFromBody
removesSkipGenericFunctions
bit - so when the compiler gets to a function node with type parameters within a context-sensitive function's return it can't just useSkipGenericFunctions
to skip over inferring its own return type from body.The goal of
SkipReturnTypeFromBodyInference
is to persist throughgetReturnTypeFromBody
.