Skip to content

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

Merged
Show file tree
Hide file tree
Changes from 4 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
143 changes: 121 additions & 22 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Copy link
Member

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.

Suggested change
return kind !== TypeFlags.Intersection ? getUnionType(types, unionReduction) : getIntersectionType(types);
return kind === TypeFlags.Union ? getUnionType(types, unionReduction) : getIntersectionType(types);

Copy link
Member Author

@weswigham weswigham Feb 9, 2021

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 the kind being undefined is synonymous with Union (to preserve the existing structure where there is no kind). Now, I don't intentionally leave kind 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).

}

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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
return createTypePredicate(first.kind, first.parameterName, first.parameterIndex, compositeType);
}

function typePredicateKindsMatch(a: TypePredicate, b: TypePredicate): boolean {
Expand Down Expand Up @@ -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;
Expand All @@ -24830,7 +24843,7 @@ namespace ts {
}
results.push(propType);
}
return getIntersectionType(results);
return getIntersectionType(results); // Same result for both union and intersection signatures
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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);
Expand Down Expand Up @@ -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]);
}

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.
Copy link
Member

Choose a reason for hiding this comment

The 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)

Copy link
Member

Choose a reason for hiding this comment

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

also: this is basically a parameter, so it should union just like the other parameters.

Copy link
Member Author

Choose a reason for hiding this comment

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

so it should union just like the other parameters.

And so we are, the comment is just an inversion of the comment that already exists in combineUnionThisParam which does the opposite.

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;
Copy link
Member

Choose a reason for hiding this comment

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

could they be named longest/shortest or longer/shorter?

Copy link
Member Author

Choose a reason for hiding this comment

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

This follows the same terminology we already use in combineUnionParameters, which combinedIntersectionParameters really closely mirrors.

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;
Copy link
Member

Choose a reason for hiding this comment

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

uhhhhhhhhh, why not just leftName || rightName? I'm not even sure it's observable, but even if it is, some name is better than arg0.

Copy link
Member Author

@weswigham weswigham Feb 9, 2021

Choose a reason for hiding this comment

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

Because that's what we merged for combineUnionParameters in #32056 :V

Also, "some name is better than arg0" isn't really true. Names carry semantic meaning, so let's say you have (index: number) => any and (object: Whatever): any - calling the result index where's it's type is number | Whatever has the potential to be misleading. Particularly problematic is when the types are the same, but semantically different, like
(length: number): any[] and (firstElement: number): any[] - yeah, number is the type of both, but there're not semantically the same! So calling both "length" or "firstElement" would be dead wrong. Now, I had a super old version of this (like, years old. insert "deja vu" theme here) that concatenated the names in cases like this, to lengthOrFirstElement, but that was shot down as too easily producing names that are unwieldy as unions grow. And thus, the preference for arg0 was born. It carries no information, thus you can draw no (potentially inaccurate) conclusion as to the usage of the parameter.

Copy link
Member

Choose a reason for hiding this comment

The 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 {
Copy link
Member

Choose a reason for hiding this comment

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

this should probably come before combineIntersectionParameters and combineIntersectionThisParam

Copy link
Member Author

Choose a reason for hiding this comment

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

This is in the same order that the very similar combineUnionParameters and combineUnionThisParam are~

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;
Copy link
Member

Choose a reason for hiding this comment

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

stupid efficiency question: Is it more efficient to have createSignature set these? (I'm guessing not, since probably composite signatures (1) are rare (2) have their own internal class.)

Copy link
Member Author

Choose a reason for hiding this comment

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

createSignature sets them to undefined already, along with every other optional field on signatures~

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 */
Expand Down
4 changes: 3 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5554,7 +5554,9 @@ namespace ts {
/* @internal */
mapper?: TypeMapper; // Instantiation mapper
/* @internal */
unionSignatures?: Signature[]; // Underlying signatures of a union signature
compositeSignatures?: Signature[]; // Underlying signatures of a union/intersection signature
/* @internal */
compositeKind?: TypeFlags; // TypeFlags.Union if the underlying signatures are from union members, otherwise TypeFlags.Intersection
/* @internal */
erasedSignatureCache?: Signature; // Erased version of signature (deferred)
/* @internal */
Expand Down
110 changes: 0 additions & 110 deletions tests/baselines/reference/callsOnComplexSignatures.errors.txt

This file was deleted.

Loading