-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Combine multiple overloads into a single contextual signature #42620
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
Changes from 4 commits
0b7a99c
c6fde09
429b88f
33a5727
88eadfd
1495f74
961bcc0
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 |
---|---|---|
|
@@ -10206,7 +10206,8 @@ namespace ts { | |
sig.resolvedMinArgumentCount = undefined; | ||
sig.target = undefined; | ||
sig.mapper = undefined; | ||
sig.unionSignatures = undefined; | ||
sig.compositeSignatures = undefined; | ||
sig.compositeKind = undefined; | ||
return sig; | ||
} | ||
|
||
|
@@ -10215,13 +10216,15 @@ namespace ts { | |
/*resolvedTypePredicate*/ undefined, sig.minArgumentCount, sig.flags & SignatureFlags.PropagatingFlags); | ||
result.target = sig.target; | ||
result.mapper = sig.mapper; | ||
result.unionSignatures = sig.unionSignatures; | ||
result.compositeSignatures = sig.compositeSignatures; | ||
result.compositeKind = sig.compositeKind; | ||
return result; | ||
} | ||
|
||
function createUnionSignature(signature: Signature, unionSignatures: Signature[]) { | ||
const result = cloneSignature(signature); | ||
result.unionSignatures = unionSignatures; | ||
result.compositeSignatures = unionSignatures; | ||
result.compositeKind = TypeFlags.Union; | ||
result.target = undefined; | ||
result.mapper = undefined; | ||
return result; | ||
|
@@ -10495,9 +10498,10 @@ namespace ts { | |
minArgCount, | ||
(left.flags | right.flags) & SignatureFlags.PropagatingFlags | ||
); | ||
result.unionSignatures = concatenate(left.unionSignatures || [left], [right]); | ||
result.compositeKind = TypeFlags.Union; | ||
result.compositeSignatures = concatenate(left.compositeKind !== TypeFlags.Intersection && left.compositeSignatures || [left], [right]); | ||
if (paramMapper) { | ||
result.mapper = left.mapper && left.unionSignatures ? combineTypeMappers(left.mapper, paramMapper) : paramMapper; | ||
result.mapper = left.compositeKind !== TypeFlags.Intersection && left.mapper && left.compositeSignatures ? combineTypeMappers(left.mapper, paramMapper) : paramMapper; | ||
} | ||
return result; | ||
} | ||
|
@@ -12015,8 +12019,8 @@ namespace ts { | |
const targetTypePredicate = getTypePredicateOfSignature(signature.target); | ||
signature.resolvedTypePredicate = targetTypePredicate ? instantiateTypePredicate(targetTypePredicate, signature.mapper!) : noTypePredicate; | ||
} | ||
else if (signature.unionSignatures) { | ||
signature.resolvedTypePredicate = getUnionTypePredicate(signature.unionSignatures) || noTypePredicate; | ||
else if (signature.compositeSignatures) { | ||
signature.resolvedTypePredicate = getUnionOrIntersectionTypePredicate(signature.compositeSignatures, signature.compositeKind) || noTypePredicate; | ||
} | ||
else { | ||
const type = signature.declaration && getEffectiveReturnTypeNode(signature.declaration); | ||
|
@@ -12045,13 +12049,17 @@ namespace ts { | |
findIndex(signature.parameters, p => p.escapedName === parameterName.escapedText), type); | ||
} | ||
|
||
function getUnionOrIntersectionType(types: Type[], kind: TypeFlags | undefined, unionReduction?: UnionReduction) { | ||
return kind !== TypeFlags.Intersection ? getUnionType(types, unionReduction) : getIntersectionType(types); | ||
} | ||
|
||
function getReturnTypeOfSignature(signature: Signature): Type { | ||
if (!signature.resolvedReturnType) { | ||
if (!pushTypeResolution(signature, TypeSystemPropertyName.ResolvedReturnType)) { | ||
return errorType; | ||
} | ||
let type = signature.target ? instantiateType(getReturnTypeOfSignature(signature.target), signature.mapper) : | ||
signature.unionSignatures ? instantiateType(getUnionType(map(signature.unionSignatures, getReturnTypeOfSignature), UnionReduction.Subtype), signature.mapper) : | ||
signature.compositeSignatures ? instantiateType(getUnionOrIntersectionType(map(signature.compositeSignatures, getReturnTypeOfSignature), signature.compositeKind, UnionReduction.Subtype), signature.mapper) : | ||
getReturnTypeFromAnnotation(signature.declaration!) || | ||
(nodeIsMissing((<FunctionLikeDeclaration>signature.declaration).body) ? anyType : getReturnTypeFromBody(<FunctionLikeDeclaration>signature.declaration)); | ||
if (signature.flags & SignatureFlags.IsInnerCallChain) { | ||
|
@@ -13481,13 +13489,18 @@ namespace ts { | |
return getUnionTypeFromSortedList(typeSet, objectFlags, aliasSymbol, aliasTypeArguments, origin); | ||
} | ||
|
||
function getUnionTypePredicate(signatures: readonly Signature[]): TypePredicate | undefined { | ||
function getUnionOrIntersectionTypePredicate(signatures: readonly Signature[], kind: TypeFlags | undefined): TypePredicate | undefined { | ||
let first: TypePredicate | undefined; | ||
const types: Type[] = []; | ||
for (const sig of signatures) { | ||
const pred = getTypePredicateOfSignature(sig); | ||
if (!pred || pred.kind === TypePredicateKind.AssertsThis || pred.kind === TypePredicateKind.AssertsIdentifier) { | ||
continue; | ||
if (kind !== TypeFlags.Intersection) { | ||
continue; | ||
} | ||
else { | ||
return; // intersections demand all members be type predicates for the result to have a predicate | ||
} | ||
} | ||
|
||
if (first) { | ||
|
@@ -13502,11 +13515,11 @@ namespace ts { | |
types.push(pred.type); | ||
} | ||
if (!first) { | ||
// No union signatures had a type predicate. | ||
// No signatures had a type predicate. | ||
return undefined; | ||
} | ||
const unionType = getUnionType(types); | ||
return createTypePredicate(first.kind, first.parameterName, first.parameterIndex, unionType); | ||
const compositeType = kind !== TypeFlags.Intersection ? getUnionType(types) : getIntersectionType(types); | ||
weswigham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return createTypePredicate(first.kind, first.parameterName, first.parameterIndex, compositeType); | ||
} | ||
|
||
function typePredicateKindsMatch(a: TypePredicate, b: TypePredicate): boolean { | ||
|
@@ -24812,14 +24825,14 @@ namespace ts { | |
} | ||
|
||
function getJsxPropsTypeForSignatureFromMember(sig: Signature, forcedLookupLocation: __String) { | ||
if (sig.unionSignatures) { | ||
if (sig.compositeSignatures) { | ||
// JSX Elements using the legacy `props`-field based lookup (eg, react class components) need to treat the `props` member as an input | ||
// instead of an output position when resolving the signature. We need to go back to the input signatures of the composite signature, | ||
// get the type of `props` on each return type individually, and then _intersect them_, rather than union them (as would normally occur | ||
// for a union signature). It's an unfortunate quirk of looking in the output of the signature for the type we want to use for the input. | ||
// The default behavior of `getTypeOfFirstParameterOfSignatureWithFallback` when no `props` member name is defined is much more sane. | ||
const results: Type[] = []; | ||
for (const signature of sig.unionSignatures) { | ||
for (const signature of sig.compositeSignatures) { | ||
const instance = getReturnTypeOfSignature(signature); | ||
if (isTypeAny(instance)) { | ||
return instance; | ||
|
@@ -24830,7 +24843,7 @@ namespace ts { | |
} | ||
results.push(propType); | ||
} | ||
return getIntersectionType(results); | ||
return getIntersectionType(results); // Same result for both union and intersection signatures | ||
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. why does this need to be commented? It didn't surprise me since no other code changed around here, so I think I'm missing the actual surprising thing. 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. The actual surprising thing is that nothing changed here. This code path handled union composite signatures... and now we're handling intersection composite signatures in the same way (rather than inverting anything), which is odd. The reason for that is explained in this comment (namely that for compat reasons we intentionally do "the wrong thing" for union signatures here, which happens to be "the right thing" for intersection signatures) |
||
} | ||
const instanceType = getReturnTypeOfSignature(sig); | ||
return isTypeAny(instanceType) ? instanceType : getTypeOfPropertyOfType(instanceType, forcedLookupLocation); | ||
|
@@ -24924,16 +24937,102 @@ namespace ts { | |
} | ||
} | ||
|
||
function getIntersectedSignatures(signatures: readonly Signature[]) { | ||
return !getStrictOptionValue(compilerOptions, "noImplicitAny") ? undefined : reduceLeft(signatures, (left, right) => left === right || !left ? left : (!!left.typeParameters && !!right.typeParameters && compareTypeParametersIdentical(left.typeParameters, right.typeParameters)) || (!left.typeParameters && !right.typeParameters) ? combineSignaturesOfIntersectionMembers(left, right) : undefined, signatures[0]); | ||
weswigham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
function combineIntersectionThisParam(left: Symbol | undefined, right: Symbol | undefined, mapper: TypeMapper | undefined): Symbol | undefined { | ||
if (!left || !right) { | ||
return left || right; | ||
} | ||
// A signature `this` type might be a read or a write position... It's very possible that it should be invariant | ||
// and we should refuse to merge signatures if there are `this` types and they do not match. However, so as to be | ||
// pessimistic when contextual typing, for now, we'll union the `this` types. | ||
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 would cause us to change this? (not a big fan of 'for now'-style comments because they don't have enough context with them) 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. also: 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.
And so we are, the comment is just an inversion of the comment that already exists in |
||
const thisType = getUnionType([getTypeOfSymbol(left), instantiateType(getTypeOfSymbol(right), mapper)]); | ||
return createSymbolWithType(left, thisType); | ||
} | ||
|
||
function combineIntersectionParameters(left: Signature, right: Signature, mapper: TypeMapper | undefined) { | ||
const leftCount = getParameterCount(left); | ||
const rightCount = getParameterCount(right); | ||
const longest = leftCount >= rightCount ? left : right; | ||
const shorter = longest === left ? right : left; | ||
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. could they be named longest/shortest or longer/shorter? 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. This follows the same terminology we already use in |
||
const longestCount = longest === left ? leftCount : rightCount; | ||
const eitherHasEffectiveRest = (hasEffectiveRestParameter(left) || hasEffectiveRestParameter(right)); | ||
const needsExtraRestElement = eitherHasEffectiveRest && !hasEffectiveRestParameter(longest); | ||
const params = new Array<Symbol>(longestCount + (needsExtraRestElement ? 1 : 0)); | ||
for (let i = 0; i < longestCount; i++) { | ||
let longestParamType = tryGetTypeAtPosition(longest, i)!; | ||
if (longest === right) { | ||
longestParamType = instantiateType(longestParamType, mapper); | ||
} | ||
let shorterParamType = tryGetTypeAtPosition(shorter, i) || unknownType; | ||
if (shorter === right) { | ||
shorterParamType = instantiateType(shorterParamType, mapper); | ||
} | ||
const unionParamType = getUnionType([longestParamType, shorterParamType]); | ||
const isRestParam = eitherHasEffectiveRest && !needsExtraRestElement && i === (longestCount - 1); | ||
const isOptional = i >= getMinArgumentCount(longest) && i >= getMinArgumentCount(shorter); | ||
const leftName = i >= leftCount ? undefined : getParameterNameAtPosition(left, i); | ||
const rightName = i >= rightCount ? undefined : getParameterNameAtPosition(right, i); | ||
|
||
const paramName = leftName === rightName ? leftName : | ||
!leftName ? rightName : | ||
!rightName ? leftName : | ||
undefined; | ||
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. uhhhhhhhhh, why not just 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. Because that's what we merged for Also, "some name is better than arg0" isn't really true. Names carry semantic meaning, so let's say you have 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. If @dragomirtitian cares enough to fix it, that's good enough for me. =) |
||
const paramSymbol = createSymbol( | ||
SymbolFlags.FunctionScopedVariable | (isOptional && !isRestParam ? SymbolFlags.Optional : 0), | ||
paramName || `arg${i}` as __String | ||
); | ||
paramSymbol.type = isRestParam ? createArrayType(unionParamType) : unionParamType; | ||
params[i] = paramSymbol; | ||
} | ||
if (needsExtraRestElement) { | ||
const restParamSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, "args" as __String); | ||
restParamSymbol.type = createArrayType(getTypeAtPosition(shorter, longestCount)); | ||
if (shorter === right) { | ||
restParamSymbol.type = instantiateType(restParamSymbol.type, mapper); | ||
} | ||
params[longestCount] = restParamSymbol; | ||
} | ||
return params; | ||
} | ||
|
||
function combineSignaturesOfIntersectionMembers(left: Signature, right: Signature): Signature { | ||
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. this should probably come before 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. This is in the same order that the very similar |
||
const typeParams = left.typeParameters || right.typeParameters; | ||
let paramMapper: TypeMapper | undefined; | ||
if (left.typeParameters && right.typeParameters) { | ||
paramMapper = createTypeMapper(right.typeParameters, left.typeParameters); | ||
// We just use the type parameter defaults from the first signature | ||
} | ||
const declaration = left.declaration; | ||
const params = combineIntersectionParameters(left, right, paramMapper); | ||
const thisParam = combineIntersectionThisParam(left.thisParameter, right.thisParameter, paramMapper); | ||
const minArgCount = Math.max(left.minArgumentCount, right.minArgumentCount); | ||
const result = createSignature( | ||
declaration, | ||
typeParams, | ||
thisParam, | ||
params, | ||
/*resolvedReturnType*/ undefined, | ||
/*resolvedTypePredicate*/ undefined, | ||
minArgCount, | ||
(left.flags | right.flags) & SignatureFlags.PropagatingFlags | ||
); | ||
result.compositeKind = TypeFlags.Intersection; | ||
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. stupid efficiency question: Is it more efficient to have 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.
|
||
result.compositeSignatures = concatenate(left.compositeKind === TypeFlags.Intersection && left.compositeSignatures || [left], [right]); | ||
if (paramMapper) { | ||
result.mapper = left.compositeKind === TypeFlags.Intersection && left.mapper && left.compositeSignatures ? combineTypeMappers(left.mapper, paramMapper) : paramMapper; | ||
} | ||
return result; | ||
} | ||
|
||
// If the given type is an object or union type with a single signature, and if that signature has at | ||
// least as many parameters as the given function, return the signature. Otherwise return undefined. | ||
function getContextualCallSignature(type: Type, node: SignatureDeclaration): Signature | undefined { | ||
const signatures = getSignaturesOfType(type, SignatureKind.Call); | ||
if (signatures.length === 1) { | ||
const signature = signatures[0]; | ||
if (!isAritySmaller(signature, node)) { | ||
return signature; | ||
} | ||
} | ||
const applicableByArity = filter(signatures, s => !isAritySmaller(s, node)); | ||
return applicableByArity.length === 1 ? applicableByArity[0] : getIntersectedSignatures(applicableByArity); | ||
} | ||
|
||
/** If the contextual signature has fewer parameters than the function expression, do not use it */ | ||
|
This file was deleted.
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.
Is this still correct? It's easier to read.
Uh oh!
There was an error while loading. Please reload this page.
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.
I intentionally made all the comparisons
kind !== TypeFlags.Intersection
so that thekind
beingundefined
is synonymous withUnion
(to preserve the existing structure where there is nokind
). Now, I don't intentionally leavekind
undefined
anywhere, but just in case, I've written the comparisons to be resilient to it (since it is an "optional" signature member, and maybe some plugin author manufactures signatures and send them into the checker, who knows).