Skip to content

Allow identical type parameter lists to merge in union signatures #31023

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
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
59 changes: 49 additions & 10 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10275,7 +10275,7 @@ namespace ts {
if (signatures !== masterList) {
const signature = signatures[0];
Debug.assert(!!signature, "getUnionSignatures bails early on empty signature lists and should not have empty lists on second pass");
results = signature.typeParameters && some(results, s => !!s.typeParameters) ? undefined : map(results, sig => combineSignaturesOfUnionMembers(sig, signature));
results = signature.typeParameters && some(results, s => !!s.typeParameters && !compareTypeParametersIdentical(signature.typeParameters!, s.typeParameters)) ? undefined : map(results, sig => combineSignaturesOfUnionMembers(sig, signature));
if (!results) {
break;
}
Expand All @@ -10286,18 +10286,39 @@ namespace ts {
return result || emptyArray;
}

function combineUnionThisParam(left: Symbol | undefined, right: Symbol | undefined): Symbol | undefined {
function compareTypeParametersIdentical(sourceParams: readonly TypeParameter[], targetParams: readonly TypeParameter[]): boolean {
if (sourceParams.length !== targetParams.length) {
return false;
}

const mapper = createTypeMapper(targetParams, sourceParams);
for (let i = 0; i < sourceParams.length; i++) {
const source = sourceParams[i];
const target = targetParams[i];
if (source === target) continue;
// We instantiate the target type parameter constraints into the source types so we can recognize `<T, U extends T>` as the same as `<A, B extends A>`
if (!isTypeIdenticalTo(getConstraintFromTypeParameter(source) || unknownType, instantiateType(getConstraintFromTypeParameter(target) || unknownType, mapper))) return false;
// We don't compare defaults - we just use the type parameter defaults from the first signature that seems to match.
// It might make sense to combine these defaults in the future, but doing so intelligently requires knowing
// if the parameter is used covariantly or contravariantly (so we intersect if it's used like a parameter or union if used like a return type)
// and, since it's just an inference _default_, just picking one arbitrarily works OK.
}

return true;
}

function combineUnionThisParam(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
// permissive when calling, for now, we'll intersect the `this` types just like we do for param types in union signatures.
const thisType = getIntersectionType([getTypeOfSymbol(left), getTypeOfSymbol(right)]);
const thisType = getIntersectionType([getTypeOfSymbol(left), instantiateType(getTypeOfSymbol(right), mapper)]);
return createSymbolWithType(left, thisType);
}

function combineUnionParameters(left: Signature, right: Signature) {
function combineUnionParameters(left: Signature, right: Signature, mapper: TypeMapper | undefined) {
const leftCount = getParameterCount(left);
const rightCount = getParameterCount(right);
const longest = leftCount >= rightCount ? left : right;
Expand All @@ -10307,8 +10328,14 @@ namespace ts {
const needsExtraRestElement = eitherHasEffectiveRest && !hasEffectiveRestParameter(longest);
const params = new Array<Symbol>(longestCount + (needsExtraRestElement ? 1 : 0));
for (let i = 0; i < longestCount; i++) {
const longestParamType = tryGetTypeAtPosition(longest, i)!;
const shorterParamType = tryGetTypeAtPosition(shorter, i) || unknownType;
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 = getIntersectionType([longestParamType, shorterParamType]);
const isRestParam = eitherHasEffectiveRest && !needsExtraRestElement && i === (longestCount - 1);
const isOptional = i >= getMinArgumentCount(longest) && i >= getMinArgumentCount(shorter);
Expand All @@ -10329,19 +10356,28 @@ namespace ts {
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 combineSignaturesOfUnionMembers(left: Signature, right: Signature): Signature {
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 = combineUnionParameters(left, right);
const thisParam = combineUnionThisParam(left.thisParameter, right.thisParameter);
const params = combineUnionParameters(left, right, paramMapper);
const thisParam = combineUnionThisParam(left.thisParameter, right.thisParameter, paramMapper);
const minArgCount = Math.max(left.minArgumentCount, right.minArgumentCount);
const result = createSignature(
declaration,
left.typeParameters || right.typeParameters,
typeParams,
thisParam,
params,
/*resolvedReturnType*/ undefined,
Expand All @@ -10350,6 +10386,9 @@ namespace ts {
(left.flags | right.flags) & SignatureFlags.PropagatingFlags
);
result.unionSignatures = concatenate(left.unionSignatures || [left], [right]);
if (paramMapper) {
result.mapper = left.mapper && left.unionSignatures ? combineTypeMappers(left.mapper, paramMapper) : paramMapper;
}
return result;
}

Expand Down Expand Up @@ -11896,7 +11935,7 @@ namespace ts {
return errorType;
}
let type = signature.target ? instantiateType(getReturnTypeOfSignature(signature.target), signature.mapper) :
signature.unionSignatures ? getUnionType(map(signature.unionSignatures, getReturnTypeOfSignature), UnionReduction.Subtype) :
signature.unionSignatures ? instantiateType(getUnionType(map(signature.unionSignatures, getReturnTypeOfSignature), UnionReduction.Subtype), signature.mapper) :
getReturnTypeFromAnnotation(signature.declaration!) ||
(nodeIsMissing((<FunctionLikeDeclaration>signature.declaration).body) ? anyType : getReturnTypeFromBody(<FunctionLikeDeclaration>signature.declaration));
if (signature.flags & SignatureFlags.IsInnerCallChain) {
Expand Down
85 changes: 85 additions & 0 deletions tests/baselines/reference/unionOfClassCalls.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
tests/cases/compiler/unionOfClassCalls.ts(28,5): error TS2349: This expression is not callable.
Each member of the union type '{ (callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number): number; (callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number; <U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: number[]) => U, initialValue: U): U; } | { (callbackfn: (previousValue: string, currentValue: string, currentIndex: number, array: string[]) => string): string; (callbackfn: (previousValue: string, currentValue: string, currentIndex: number, array: string[]) => string, initialValue: string): string; <U>(callbackfn: (previousValue: U, currentValue: string, currentIndex: number, array: string[]) => U, initialValue: U): U; }' has signatures, but none of those signatures are compatible with each other.


==== tests/cases/compiler/unionOfClassCalls.ts (1 errors) ====
// from https://github.com/microsoft/TypeScript/issues/30717
declare class Test<T> {
obj: T;
get<K extends keyof T>(k: K): T[K];
}

interface A { t: "A" }
interface B { t: "B" }

declare const tmp: Test<A> | Test<B>;

switch (tmp.get('t')) {
case 'A': break;
case 'B': break;
}

// from https://github.com/microsoft/TypeScript/issues/36390

const arr: number[] | string[] = []; // Works with Array<number | string>
const arr1: number[] = [];
const arr2: string[] = [];

arr.map((a: number | string, index: number) => {
return index
})

// This case still doesn't work because `reduce` has multiple overloads :(
arr.reduce((acc: Array<string>, a: number | string, index: number) => {
~~~~~~
!!! error TS2349: This expression is not callable.
!!! error TS2349: Each member of the union type '{ (callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number): number; (callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number; <U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: number[]) => U, initialValue: U): U; } | { (callbackfn: (previousValue: string, currentValue: string, currentIndex: number, array: string[]) => string): string; (callbackfn: (previousValue: string, currentValue: string, currentIndex: number, array: string[]) => string, initialValue: string): string; <U>(callbackfn: (previousValue: U, currentValue: string, currentIndex: number, array: string[]) => U, initialValue: U): U; }' has signatures, but none of those signatures are compatible with each other.
return []
}, [])

arr.forEach((a: number | string, index: number) => {
return index
})

arr1.map((a: number, index: number) => {
return index
})

arr1.reduce((acc: number[], a: number, index: number) => {
return [a]
}, [])

arr1.forEach((a: number, index: number) => {
return index
})
arr2.map((a: string, index: number) => {
return index
})

arr2.reduce((acc: string[], a: string, index: number) => {
return []
}, [])

arr2.forEach((a: string, index: number) => {
return index
})

// from https://github.com/microsoft/TypeScript/issues/36307

declare class Foo {
doThing(): Promise<this>
}

declare class Bar extends Foo {
bar: number;
}
declare class Baz extends Foo {
baz: number;
}

declare var a: Bar | Baz;
// note, you must annotate `result` for now
a.doThing().then((result: Bar | Baz) => {
// whatever
});

121 changes: 121 additions & 0 deletions tests/baselines/reference/unionOfClassCalls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//// [unionOfClassCalls.ts]
// from https://github.com/microsoft/TypeScript/issues/30717
declare class Test<T> {
obj: T;
get<K extends keyof T>(k: K): T[K];
}

interface A { t: "A" }
interface B { t: "B" }

declare const tmp: Test<A> | Test<B>;

switch (tmp.get('t')) {
case 'A': break;
case 'B': break;
}

// from https://github.com/microsoft/TypeScript/issues/36390

const arr: number[] | string[] = []; // Works with Array<number | string>
const arr1: number[] = [];
const arr2: string[] = [];

arr.map((a: number | string, index: number) => {
return index
})

// This case still doesn't work because `reduce` has multiple overloads :(
arr.reduce((acc: Array<string>, a: number | string, index: number) => {
return []
}, [])

arr.forEach((a: number | string, index: number) => {
return index
})

arr1.map((a: number, index: number) => {
return index
})

arr1.reduce((acc: number[], a: number, index: number) => {
return [a]
}, [])

arr1.forEach((a: number, index: number) => {
return index
})
arr2.map((a: string, index: number) => {
return index
})

arr2.reduce((acc: string[], a: string, index: number) => {
return []
}, [])

arr2.forEach((a: string, index: number) => {
return index
})

// from https://github.com/microsoft/TypeScript/issues/36307

declare class Foo {
doThing(): Promise<this>
}

declare class Bar extends Foo {
bar: number;
}
declare class Baz extends Foo {
baz: number;
}

declare var a: Bar | Baz;
// note, you must annotate `result` for now
a.doThing().then((result: Bar | Baz) => {
// whatever
});


//// [unionOfClassCalls.js]
"use strict";
switch (tmp.get('t')) {
case 'A': break;
case 'B': break;
}
// from https://github.com/microsoft/TypeScript/issues/36390
var arr = []; // Works with Array<number | string>
var arr1 = [];
var arr2 = [];
arr.map(function (a, index) {
return index;
});
// This case still doesn't work because `reduce` has multiple overloads :(
arr.reduce(function (acc, a, index) {
return [];
}, []);
arr.forEach(function (a, index) {
return index;
});
arr1.map(function (a, index) {
return index;
});
arr1.reduce(function (acc, a, index) {
return [a];
}, []);
arr1.forEach(function (a, index) {
return index;
});
arr2.map(function (a, index) {
return index;
});
arr2.reduce(function (acc, a, index) {
return [];
}, []);
arr2.forEach(function (a, index) {
return index;
});
// note, you must annotate `result` for now
a.doThing().then(function (result) {
// whatever
});
Loading