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

Limit type argument inference from binding patterns #49086

Merged
Merged
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
72 changes: 42 additions & 30 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26517,7 +26517,7 @@ namespace ts {
if (result) {
return result;
}
if (!(contextFlags! & ContextFlags.SkipBindingPatterns) && isBindingPattern(declaration.name)) { // This is less a contextual type and more an implied shape - in some cases, this may be undesirable
if (!(contextFlags! & ContextFlags.SkipBindingPatterns) && isBindingPattern(declaration.name) && declaration.name.elements.length > 0) {
return getTypeFromBindingPattern(declaration.name, /*includePatternInType*/ true, /*reportErrors*/ false);
}
}
Expand Down Expand Up @@ -27051,22 +27051,20 @@ namespace ts {
const inferenceContext = getInferenceContext(node);
// If no inferences have been made, nothing is gained from instantiating as type parameters
// would just be replaced with their defaults similar to the apparent type.
if (inferenceContext && some(inferenceContext.inferences, hasInferenceCandidates)) {
if (inferenceContext && contextFlags! & ContextFlags.Signature && some(inferenceContext.inferences, hasInferenceCandidates)) {
// For contextual signatures we incorporate all inferences made so far, e.g. from return
// types as well as arguments to the left in a function call.
if (contextFlags && contextFlags & ContextFlags.Signature) {
return instantiateInstantiableTypes(contextualType, inferenceContext.nonFixingMapper);
}
return instantiateInstantiableTypes(contextualType, inferenceContext.nonFixingMapper);
}
if (inferenceContext?.returnMapper) {
Copy link
Member

Choose a reason for hiding this comment

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

This if statement now runs even when there are no inference candidates. Is that intended?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, because the returnMapper pulls from a different inference context that may have candidates not present in inferenceContext. (Also, returnMapper is guaranteed to have candidates if it exists.)

// For other purposes (e.g. determining whether to produce literal types) we only
// incorporate inferences made from the return type in a function call. We remove
// the 'boolean' type from the contextual type such that contextually typed boolean
// literals actually end up widening to 'boolean' (see #48363).
if (inferenceContext.returnMapper) {
const type = instantiateInstantiableTypes(contextualType, inferenceContext.returnMapper);
return type.flags & TypeFlags.Union && containsType((type as UnionType).types, regularFalseType) && containsType((type as UnionType).types, regularTrueType) ?
filterType(type, t => t !== regularFalseType && t !== regularTrueType) :
type;
}
const type = instantiateInstantiableTypes(contextualType, inferenceContext.returnMapper);
return type.flags & TypeFlags.Union && containsType((type as UnionType).types, regularFalseType) && containsType((type as UnionType).types, regularTrueType) ?
filterType(type, t => t !== regularFalseType && t !== regularTrueType) :
type;
}
}
return contextualType;
Expand Down Expand Up @@ -29902,29 +29900,43 @@ namespace ts {
// 'let f: (x: string) => number = wrap(s => s.length)', we infer from the declared type of 'f' to the
// return type of 'wrap'.
if (node.kind !== SyntaxKind.Decorator) {
const contextualType = getContextualType(node, every(signature.typeParameters, p => !!getDefaultFromTypeParameter(p)) ? ContextFlags.SkipBindingPatterns : ContextFlags.None);
const skipBindingPatterns = every(signature.typeParameters, p => !!getDefaultFromTypeParameter(p));
const contextualType = getContextualType(node, skipBindingPatterns ? ContextFlags.SkipBindingPatterns : ContextFlags.None);
if (contextualType) {
const inferenceTargetType = getReturnTypeOfSignature(signature);
if (couldContainTypeVariables(inferenceTargetType)) {
// We clone the inference context to avoid disturbing a resolution in progress for an
// outer call expression. Effectively we just want a snapshot of whatever has been
// inferred for any outer call expression so far.
const outerContext = getInferenceContext(node);
const outerMapper = getMapperFromContext(cloneInferenceContext(outerContext, InferenceFlags.NoDefault));
const instantiatedType = instantiateType(contextualType, outerMapper);
// If the contextual type is a generic function type with a single call signature, we
// instantiate the type with its own type parameters and type arguments. This ensures that
// the type parameters are not erased to type any during type inference such that they can
// be inferred as actual types from the contextual type. For example:
// declare function arrayMap<T, U>(f: (x: T) => U): (a: T[]) => U[];
// const boxElements: <A>(a: A[]) => { value: A }[] = arrayMap(value => ({ value }));
// Above, the type of the 'value' parameter is inferred to be 'A'.
const contextualSignature = getSingleCallSignature(instantiatedType);
const inferenceSourceType = contextualSignature && contextualSignature.typeParameters ?
getOrCreateTypeFromSignature(getSignatureInstantiationWithoutFillingInTypeArguments(contextualSignature, contextualSignature.typeParameters)) :
instantiatedType;
// Inferences made from return types have lower priority than all other inferences.
inferTypes(context.inferences, inferenceSourceType, inferenceTargetType, InferencePriority.ReturnType);
const isFromBindingPattern = !skipBindingPatterns && getContextualType(node, ContextFlags.SkipBindingPatterns) !== contextualType;
// A return type inference from a binding pattern can be used in instantiating the contextual
// type of an argument later in inference, but cannot stand on its own as the final return type.
// It is incorporated into `context.returnMapper` which is used in `instantiateContextualType`,
// but doesn't need to go into `context.inferences`. This allows a an array binding pattern to
// produce a tuple for `T` in
// declare function f<T>(cb: () => T): T;
// const [e1, e2, e3] = f(() => [1, "hi", true]);
// but does not produce any inference for `T` in
// declare function f<T>(): T;
// const [e1, e2, e3] = f();
if (!isFromBindingPattern) {
// We clone the inference context to avoid disturbing a resolution in progress for an
// outer call expression. Effectively we just want a snapshot of whatever has been
// inferred for any outer call expression so far.
const outerMapper = getMapperFromContext(cloneInferenceContext(outerContext, InferenceFlags.NoDefault));
const instantiatedType = instantiateType(contextualType, outerMapper);
// If the contextual type is a generic function type with a single call signature, we
// instantiate the type with its own type parameters and type arguments. This ensures that
// the type parameters are not erased to type any during type inference such that they can
// be inferred as actual types from the contextual type. For example:
// declare function arrayMap<T, U>(f: (x: T) => U): (a: T[]) => U[];
// const boxElements: <A>(a: A[]) => { value: A }[] = arrayMap(value => ({ value }));
// Above, the type of the 'value' parameter is inferred to be 'A'.
const contextualSignature = getSingleCallSignature(instantiatedType);
const inferenceSourceType = contextualSignature && contextualSignature.typeParameters ?
getOrCreateTypeFromSignature(getSignatureInstantiationWithoutFillingInTypeArguments(contextualSignature, contextualSignature.typeParameters)) :
instantiatedType;
// Inferences made from return types have lower priority than all other inferences.
inferTypes(context.inferences, inferenceSourceType, inferenceTargetType, InferencePriority.ReturnType);
}
// Create a type mapper for instantiating generic contextual types using the inferences made
// from the return type. We need a separate inference pass here because (a) instantiation of
// the source type uses the outer context's return mapper (which excludes inferences made from
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5867,7 +5867,7 @@ namespace ts {
ReturnType = 1 << 7, // Inference made from return type of generic function
LiteralKeyof = 1 << 8, // Inference made from a string literal to a keyof T
NoConstraints = 1 << 9, // Don't infer from constraints of instantiable types
AlwaysStrict = 1 << 10, // Always use strict rules for contravariant inferences
AlwaysStrict = 1 << 10, // Always use strict rules for contravariant inferences
MaxValue = 1 << 11, // Seed for inference priority tracking

PriorityImpliesCombination = ReturnType | MappedTypeConstraint | LiteralKeyof, // These priorities imply that the resulting type should be a combination of all candidates
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
tests/cases/compiler/bindingPatternCannotBeOnlyInferenceSource.ts(2,7): error TS2571: Object is of type 'unknown'.
tests/cases/compiler/bindingPatternCannotBeOnlyInferenceSource.ts(3,9): error TS2339: Property 'p1' does not exist on type 'unknown'.
tests/cases/compiler/bindingPatternCannotBeOnlyInferenceSource.ts(4,7): error TS2461: Type 'unknown' is not an array type.
tests/cases/compiler/bindingPatternCannotBeOnlyInferenceSource.ts(4,7): error TS2571: Object is of type 'unknown'.
tests/cases/compiler/bindingPatternCannotBeOnlyInferenceSource.ts(5,7): error TS2461: Type 'unknown' is not an array type.


==== tests/cases/compiler/bindingPatternCannotBeOnlyInferenceSource.ts (5 errors) ====
declare function f<T>(): T;
const {} = f(); // error (only in strictNullChecks)
~~
!!! error TS2571: Object is of type 'unknown'.
const { p1 } = f(); // error
~~
!!! error TS2339: Property 'p1' does not exist on type 'unknown'.
const [] = f(); // error
~~
!!! error TS2461: Type 'unknown' is not an array type.
~~
!!! error TS2571: Object is of type 'unknown'.
const [e1, e2] = f(); // error
~~~~~~~~
!!! error TS2461: Type 'unknown' is not an array type.

// Repro from #43605
type Dispatch<A = { type: any; [extraProps: string]: any }> = { <T extends A>(action: T): T };
type IFuncs = { readonly [key: string]: (...p: any) => void };
type IDestructuring<T extends IFuncs> = { readonly [key in keyof T]?: (...p: Parameters<T[key]>) => void };
type Destructuring<T extends IFuncs, U extends IDestructuring<T>> = (dispatch: Dispatch<any>, funcs: T) => U;
const funcs1 = {
funcA: (a: boolean): void => {},
funcB: (b: string, bb: string): void => {},
funcC: (c: number, cc: number, ccc: boolean): void => {},
};
type TFuncs1 = typeof funcs1;
declare function useReduxDispatch1<T extends IDestructuring<TFuncs1>>(destructuring: Destructuring<TFuncs1, T>): T;
const {} = useReduxDispatch1(
(d, f) => ({
funcA: (...p) => d(f.funcA(...p)), // p should be inferrable
funcB: (...p) => d(f.funcB(...p)),
funcC: (...p) => d(f.funcC(...p)),
})
);

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//// [bindingPatternCannotBeOnlyInferenceSource.ts]
declare function f<T>(): T;
const {} = f(); // error (only in strictNullChecks)
const { p1 } = f(); // error
const [] = f(); // error
const [e1, e2] = f(); // error

// Repro from #43605
type Dispatch<A = { type: any; [extraProps: string]: any }> = { <T extends A>(action: T): T };
type IFuncs = { readonly [key: string]: (...p: any) => void };
type IDestructuring<T extends IFuncs> = { readonly [key in keyof T]?: (...p: Parameters<T[key]>) => void };
type Destructuring<T extends IFuncs, U extends IDestructuring<T>> = (dispatch: Dispatch<any>, funcs: T) => U;
const funcs1 = {
funcA: (a: boolean): void => {},
funcB: (b: string, bb: string): void => {},
funcC: (c: number, cc: number, ccc: boolean): void => {},
};
type TFuncs1 = typeof funcs1;
declare function useReduxDispatch1<T extends IDestructuring<TFuncs1>>(destructuring: Destructuring<TFuncs1, T>): T;
const {} = useReduxDispatch1(
(d, f) => ({
funcA: (...p) => d(f.funcA(...p)), // p should be inferrable
funcB: (...p) => d(f.funcB(...p)),
funcC: (...p) => d(f.funcC(...p)),
})
);


//// [bindingPatternCannotBeOnlyInferenceSource.js]
var _a = f(); // error (only in strictNullChecks)
var p1 = f().p1; // error
var _b = f(); // error
var _c = f(), e1 = _c[0], e2 = _c[1]; // error
var funcs1 = {
funcA: function (a) { },
funcB: function (b, bb) { },
funcC: function (c, cc, ccc) { }
};
var _d = useReduxDispatch1(function (d, f) { return ({
funcA: function () {
var p = [];
for (var _i = 0; _i < arguments.length; _i++) {
p[_i] = arguments[_i];
}
return d(f.funcA.apply(f, p));
},
funcB: function () {
var p = [];
for (var _i = 0; _i < arguments.length; _i++) {
p[_i] = arguments[_i];
}
return d(f.funcB.apply(f, p));
},
funcC: function () {
var p = [];
for (var _i = 0; _i < arguments.length; _i++) {
p[_i] = arguments[_i];
}
return d(f.funcC.apply(f, p));
}
}); });
Loading