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

Infer from context sensitive return expressions #60909

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
43 changes: 33 additions & 10 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

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 because getReturnTypeFromBody removes SkipGenericFunctions bit - so when the compiler gets to a function node with type parameters within a context-sensitive function's return it can't just use SkipGenericFunctions to skip over inferring its own return type from body.

The goal of SkipReturnTypeFromBodyInference is to persist through getReturnTypeFromBody.

// 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 */
Expand Down Expand Up @@ -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)) {
Expand All @@ -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) {
Expand All @@ -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!);
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
}
Expand Down
295 changes: 295 additions & 0 deletions tests/baselines/reference/InferFromReturnsInContextSensitive1.js
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];
Loading
Loading