Skip to content

Assign contextual parameter types to functions with type parameters #61792

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

Closed
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
19 changes: 16 additions & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21088,7 +21088,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function hasContextSensitiveReturnExpression(node: FunctionLikeDeclaration) {
if (node.typeParameters || getEffectiveReturnTypeNode(node) || !node.body) {
if (getEffectiveReturnTypeNode(node) || !node.body) {
return false;
}
if (node.body.kind !== SyntaxKind.Block) {
Expand Down Expand Up @@ -38452,6 +38452,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function inferFromAnnotatedParametersAndReturn(signature: Signature, context: Signature, inferenceContext: InferenceContext) {
if (inferenceContext.flags & InferenceFlags.IgnoreAnnotatedParametersAndReturns) {
return;
}
const len = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0);
for (let i = 0; i < len; i++) {
const declaration = signature.parameters[i].valueDeclaration as ParameterDeclaration;
Expand Down Expand Up @@ -39460,9 +39463,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (!signature) {
return;
}
let saveInferenceFlags: InferenceFlags = InferenceFlags.None;
const inferenceContext = getInferenceContext(node);
if (inferenceContext) {
saveInferenceFlags = inferenceContext.flags;
// inferring from those could possibly infer inner type parameters into outer inference context
// and they could potentially leak in the final inferred type as `instantiateTypeWithSingleGenericCallSignature`
// can only "hoist" them using `context.inferredTypeParameters` in a narrow set of cases
inferenceContext.flags |= node.typeParameters ? InferenceFlags.IgnoreAnnotatedParametersAndReturns : InferenceFlags.None;
}
if (isContextSensitive(node)) {
if (contextualSignature) {
const inferenceContext = getInferenceContext(node);
let instantiatedContextualSignature: Signature | undefined;
if (checkMode && checkMode & CheckMode.Inferential) {
inferFromAnnotatedParametersAndReturn(signature, contextualSignature, inferenceContext!);
Expand All @@ -39481,7 +39492,6 @@ 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) {
inferFromAnnotatedParametersAndReturn(signature, contextualSignature, inferenceContext!);
}
Expand All @@ -39492,6 +39502,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
signature.resolvedReturnType = returnType;
}
}
if (inferenceContext) {
inferenceContext.flags = saveInferenceFlags;
}
checkSignatureDeclaration(node);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7129,6 +7129,7 @@ export const enum InferenceFlags {
NoDefault = 1 << 0, // Infer silentNeverType for no inferences (otherwise anyType or unknownType)
AnyDefault = 1 << 1, // Infer anyType (in JS files) for no inferences (otherwise unknownType)
SkippedGenericFunction = 1 << 2, // A generic function was skipped during inference
IgnoreAnnotatedParametersAndReturns = 1 << 3, // used when assigning contextual parameters types within generic functions
}

/**
Expand Down
21 changes: 9 additions & 12 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10814,20 +10814,17 @@ export function getContainingNodeArray(node: Node): NodeArray<Node> | undefined

/** @internal */
export function hasContextSensitiveParameters(node: FunctionLikeDeclaration): boolean {
// Functions with type parameters are not context sensitive.
if (!node.typeParameters) {
// Functions with any parameters that lack type annotations are context sensitive.
if (some(node.parameters, p => !getEffectiveTypeAnnotationNode(p))) {
// Functions with any parameters that lack type annotations are context sensitive.
if (some(node.parameters, p => !getEffectiveTypeAnnotationNode(p))) {
return true;
}
if (node.kind !== SyntaxKind.ArrowFunction) {
// If the first parameter is not an explicit 'this' parameter, then the function has
// an implicit 'this' parameter which is subject to contextual typing.
const parameter = firstOrUndefined(node.parameters);
if (!(parameter && parameterIsThisKeyword(parameter))) {
return true;
}
if (node.kind !== SyntaxKind.ArrowFunction) {
// If the first parameter is not an explicit 'this' parameter, then the function has
// an implicit 'this' parameter which is subject to contextual typing.
const parameter = firstOrUndefined(node.parameters);
if (!(parameter && parameterIsThisKeyword(parameter))) {
return true;
}
}
}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
contextualTypingGenericFunction2.ts(55,3): error TS2322: Type 'number' is not assignable to type 'boolean'.
contextualTypingGenericFunction2.ts(65,3): error TS2322: Type '<N>(params: N) => (a: number, b: unknown) => boolean' is not assignable to type '(params: unknown) => (context: number, params: unknown) => number'.
Call signature return types '(a: number, b: unknown) => boolean' and '(context: number, params: unknown) => number' are incompatible.
Type 'boolean' is not assignable to type 'number'.


==== contextualTypingGenericFunction2.ts (2 errors) ====
// https://github.com/microsoft/TypeScript/issues/61791

declare const fn1: <T, Args extends Array<any>, Ret>(
self: T,
body: (this: T, ...args: Args) => Ret,
) => (...args: Args) => Ret;

export const result1 = fn1({ message: "foo" }, function (n: number) {
this.message;
});

export const result2 = fn1({ message: "foo" }, function <N>(n: N) {
this.message;
});

declare const fn2: <Args extends Array<any>, Ret>(
body: (first: string, ...args: Args) => Ret,
) => (...args: Args) => Ret;

export const result3 = fn2(function <N>(first, n: N) {});

declare const fn3: <Args extends Array<any>, Ret>(
body: (...args: Args) => (arg: string) => Ret,
) => (...args: Args) => Ret;

export const result4 = fn3(function <N>(n: N) {
return (arg) => {
return 10
}
});

declare function fn4<T, P>(config: {
context: T;
callback: (params: P) => (context: T, params: P) => number;
other?: (arg: string) => void;
}): (params: P) => number;

export const result5 = fn4({
context: 1,
callback: <N,>(params: N) => {
return (a, b) => a + 1;
},
});

export const result6 = fn4({
context: 1,
callback: <N,>(params: N) => {
return (a, b) => a + 1;
},
other: (_) => {} // outer context-sensitive function
});

// should error
export const result7 = fn4({
context: 1,
~~~~~~~
!!! error TS2322: Type 'number' is not assignable to type 'boolean'.
!!! related TS6500 contextualTypingGenericFunction2.ts:33:3: The expected type comes from property 'context' which is declared here on type '{ context: boolean; callback: (params: unknown) => (context: boolean, params: unknown) => number; other?: ((arg: string) => void) | undefined; }'
callback: <N,>(params: N) => {
return (a: boolean, b) => a ? 1 : 2;
},
other: (_) => {} // outer context-sensitive function
});

// should error
export const result8 = fn4({
context: 1,
callback: <N,>(params: N) => {
~~~~~~~~
!!! error TS2322: Type '<N>(params: N) => (a: number, b: unknown) => boolean' is not assignable to type '(params: unknown) => (context: number, params: unknown) => number'.
!!! error TS2322: Call signature return types '(a: number, b: unknown) => boolean' and '(context: number, params: unknown) => number' are incompatible.
!!! error TS2322: Type 'boolean' is not assignable to type 'number'.
!!! related TS6500 contextualTypingGenericFunction2.ts:34:3: The expected type comes from property 'callback' which is declared here on type '{ context: number; callback: (params: unknown) => (context: number, params: unknown) => number; other?: ((arg: string) => void) | undefined; }'
return (a, b) => true;
},
other: (_) => {} // outer context-sensitive function
});

declare const fnGen1: <T, Args extends Array<any>, Ret>(
self: T,
body: (this: T, ...args: Args) => Generator<any, Ret, never>,
) => (...args: Args) => Ret;

export const result9 = fnGen1({ message: "foo" }, function* (n: number) {
this.message;
});

export const result10 = fnGen1({ message: "foo" }, function* <N>(n: N) {
this.message;
});

100 changes: 100 additions & 0 deletions tests/baselines/reference/contextualTypingGenericFunction2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//// [tests/cases/compiler/contextualTypingGenericFunction2.ts] ////

//// [contextualTypingGenericFunction2.ts]
// https://github.com/microsoft/TypeScript/issues/61791

declare const fn1: <T, Args extends Array<any>, Ret>(
self: T,
body: (this: T, ...args: Args) => Ret,
) => (...args: Args) => Ret;

export const result1 = fn1({ message: "foo" }, function (n: number) {
this.message;
});

export const result2 = fn1({ message: "foo" }, function <N>(n: N) {
this.message;
});

declare const fn2: <Args extends Array<any>, Ret>(
body: (first: string, ...args: Args) => Ret,
) => (...args: Args) => Ret;

export const result3 = fn2(function <N>(first, n: N) {});

declare const fn3: <Args extends Array<any>, Ret>(
body: (...args: Args) => (arg: string) => Ret,
) => (...args: Args) => Ret;

export const result4 = fn3(function <N>(n: N) {
return (arg) => {
return 10
}
});

declare function fn4<T, P>(config: {
context: T;
callback: (params: P) => (context: T, params: P) => number;
other?: (arg: string) => void;
}): (params: P) => number;

export const result5 = fn4({
context: 1,
callback: <N,>(params: N) => {
return (a, b) => a + 1;
},
});

export const result6 = fn4({
context: 1,
callback: <N,>(params: N) => {
return (a, b) => a + 1;
},
other: (_) => {} // outer context-sensitive function
});

// should error
export const result7 = fn4({
context: 1,
callback: <N,>(params: N) => {
return (a: boolean, b) => a ? 1 : 2;
},
other: (_) => {} // outer context-sensitive function
});

// should error
export const result8 = fn4({
context: 1,
callback: <N,>(params: N) => {
return (a, b) => true;
},
other: (_) => {} // outer context-sensitive function
});

declare const fnGen1: <T, Args extends Array<any>, Ret>(
self: T,
body: (this: T, ...args: Args) => Generator<any, Ret, never>,
) => (...args: Args) => Ret;

export const result9 = fnGen1({ message: "foo" }, function* (n: number) {
this.message;
});

export const result10 = fnGen1({ message: "foo" }, function* <N>(n: N) {
this.message;
});




//// [contextualTypingGenericFunction2.d.ts]
export declare const result1: (n: number) => void;
export declare const result2: (n: any) => void;
export declare const result3: <N>(n: N) => void;
export declare const result4: <N>(n: N) => number;
export declare const result5: (params: unknown) => number;
export declare const result6: (params: unknown) => number;
export declare const result7: (params: unknown) => number;
export declare const result8: (params: unknown) => number;
export declare const result9: (n: number) => void;
export declare const result10: (n: any) => void;
Loading