Skip to content

Commit 08022d5

Browse files
authored
Allow calls on unions of dissimilar signatures (#29011)
* Add core of new union signature logic and test - needs intersection signature logic to fully work * Add inversion of variance for class props lookup from union sig returns * Fix lints * Combine parameter names for nicer quick info * PR feedback 1 * Fix miscopy * PR feedback round 2 * Remove argument name combining because loc :( * Nit cleanup round 3 * Reinline getTupleTypeForArgumentAtPos * Remove a tad more * No step on sneky off-by-one error
1 parent ab2a38e commit 08022d5

18 files changed

+1364
-47
lines changed

src/compiler/checker.ts

+107
Original file line numberDiff line numberDiff line change
@@ -6799,7 +6799,12 @@ namespace ts {
67996799
// type is the union of the constituent return types.
68006800
function getUnionSignatures(signatureLists: ReadonlyArray<ReadonlyArray<Signature>>): Signature[] {
68016801
let result: Signature[] | undefined;
6802+
let indexWithLengthOverOne: number | undefined;
68026803
for (let i = 0; i < signatureLists.length; i++) {
6804+
if (signatureLists[i].length === 0) return emptyArray;
6805+
if (signatureLists[i].length > 1) {
6806+
indexWithLengthOverOne = indexWithLengthOverOne === undefined ? i : -1; // -1 is a signal there are multiple overload sets
6807+
}
68036808
for (const signature of signatureLists[i]) {
68046809
// Only process signatures with parameter lists that aren't already in the result list
68056810
if (!result || !findMatchingSignature(result, signature, /*partialMatch*/ false, /*ignoreThisTypes*/ true, /*ignoreReturnTypes*/ true)) {
@@ -6823,9 +6828,91 @@ namespace ts {
68236828
}
68246829
}
68256830
}
6831+
if (!length(result) && indexWithLengthOverOne !== -1) {
6832+
// No sufficiently similar signature existed to subsume all the other signatures in the union - time to see if we can make a single
6833+
// signature that handles all over them. We only do this when there are overloads in only one constituent.
6834+
// (Overloads are conditional in nature and having overloads in multiple constituents would necessitate making a power set of
6835+
// signatures from the type, whose ordering would be non-obvious)
6836+
const masterList = signatureLists[indexWithLengthOverOne !== undefined ? indexWithLengthOverOne : 0];
6837+
let results: Signature[] | undefined = masterList.slice();
6838+
for (const signatures of signatureLists) {
6839+
if (signatures !== masterList) {
6840+
const signature = signatures[0];
6841+
Debug.assert(!!signature, "getUnionSignatures bails early on empty signature lists and should not have empty lists on second pass");
6842+
results = signature.typeParameters && some(results, s => !!s.typeParameters) ? undefined : map(results, sig => combineSignaturesOfUnionMembers(sig, signature));
6843+
if (!results) {
6844+
break;
6845+
}
6846+
}
6847+
}
6848+
result = results;
6849+
}
68266850
return result || emptyArray;
68276851
}
68286852

6853+
function combineUnionThisParam(left: Symbol | undefined, right: Symbol | undefined): Symbol | undefined {
6854+
if (!left || !right) {
6855+
return left || right;
6856+
}
6857+
// A signature `this` type might be a read or a write position... It's very possible that it should be invariant
6858+
// and we should refuse to merge signatures if there are `this` types and they do not match. However, so as to be
6859+
// permissive when calling, for now, we'll union the `this` types just like the overlapping-union-signature check does
6860+
const thisType = getUnionType([getTypeOfSymbol(left), getTypeOfSymbol(right)], UnionReduction.Subtype);
6861+
return createSymbolWithType(left, thisType);
6862+
}
6863+
6864+
function combineUnionParameters(left: Signature, right: Signature) {
6865+
const longest = getParameterCount(left) >= getParameterCount(right) ? left : right;
6866+
const shorter = longest === left ? right : left;
6867+
const longestCount = getParameterCount(longest);
6868+
const eitherHasEffectiveRest = (hasEffectiveRestParameter(left) || hasEffectiveRestParameter(right));
6869+
const needsExtraRestElement = eitherHasEffectiveRest && !hasEffectiveRestParameter(longest);
6870+
const params = new Array<Symbol>(longestCount + (needsExtraRestElement ? 1 : 0));
6871+
for (let i = 0; i < longestCount; i++) {
6872+
const longestParamType = tryGetTypeAtPosition(longest, i)!;
6873+
const shorterParamType = tryGetTypeAtPosition(shorter, i) || unknownType;
6874+
const unionParamType = getIntersectionType([longestParamType, shorterParamType]);
6875+
const isRestParam = eitherHasEffectiveRest && !needsExtraRestElement && i === (longestCount - 1);
6876+
const isOptional = i >= getMinArgumentCount(longest) && i >= getMinArgumentCount(shorter);
6877+
const leftName = getParameterNameAtPosition(left, i);
6878+
const rightName = getParameterNameAtPosition(right, i);
6879+
const paramSymbol = createSymbol(
6880+
SymbolFlags.FunctionScopedVariable | (isOptional && !isRestParam ? SymbolFlags.Optional : 0),
6881+
leftName === rightName ? leftName : `arg${i}` as __String
6882+
);
6883+
paramSymbol.type = isRestParam ? createArrayType(unionParamType) : unionParamType;
6884+
params[i] = paramSymbol;
6885+
}
6886+
if (needsExtraRestElement) {
6887+
const restParamSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, "args" as __String);
6888+
restParamSymbol.type = createArrayType(getTypeAtPosition(shorter, longestCount));
6889+
params[longestCount] = restParamSymbol;
6890+
}
6891+
return params;
6892+
}
6893+
6894+
function combineSignaturesOfUnionMembers(left: Signature, right: Signature): Signature {
6895+
const declaration = left.declaration;
6896+
const params = combineUnionParameters(left, right);
6897+
const thisParam = combineUnionThisParam(left.thisParameter, right.thisParameter);
6898+
const minArgCount = Math.max(left.minArgumentCount, right.minArgumentCount);
6899+
const hasRestParam = left.hasRestParameter || right.hasRestParameter;
6900+
const hasLiteralTypes = left.hasLiteralTypes || right.hasLiteralTypes;
6901+
const result = createSignature(
6902+
declaration,
6903+
left.typeParameters || right.typeParameters,
6904+
thisParam,
6905+
params,
6906+
/*resolvedReturnType*/ undefined,
6907+
/*resolvedTypePredicate*/ undefined,
6908+
minArgCount,
6909+
hasRestParam,
6910+
hasLiteralTypes
6911+
);
6912+
result.unionSignatures = concatenate(left.unionSignatures || [left], [right]);
6913+
return result;
6914+
}
6915+
68296916
function getUnionIndexInfo(types: ReadonlyArray<Type>, kind: IndexKind): IndexInfo | undefined {
68306917
const indexTypes: Type[] = [];
68316918
let isAnyReadonly = false;
@@ -17566,6 +17653,26 @@ namespace ts {
1756617653
}
1756717654

1756817655
function getJsxPropsTypeForSignatureFromMember(sig: Signature, forcedLookupLocation: __String) {
17656+
if (sig.unionSignatures) {
17657+
// JSX Elements using the legacy `props`-field based lookup (eg, react class components) need to treat the `props` member as an input
17658+
// instead of an output position when resolving the signature. We need to go back to the input signatures of the composite signature,
17659+
// get the type of `props` on each return type individually, and then _intersect them_, rather than union them (as would normally occur
17660+
// 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.
17661+
// The default behavior of `getTypeOfFirstParameterOfSignatureWithFallback` when no `props` member name is defined is much more sane.
17662+
const results: Type[] = [];
17663+
for (const signature of sig.unionSignatures) {
17664+
const instance = getReturnTypeOfSignature(signature);
17665+
if (isTypeAny(instance)) {
17666+
return instance;
17667+
}
17668+
const propType = getTypeOfPropertyOfType(instance, forcedLookupLocation);
17669+
if (!propType) {
17670+
return;
17671+
}
17672+
results.push(propType);
17673+
}
17674+
return getIntersectionType(results);
17675+
}
1756917676
const instanceType = getReturnTypeOfSignature(sig);
1757017677
return isTypeAny(instanceType) ? instanceType : getTypeOfPropertyOfType(instanceType, forcedLookupLocation);
1757117678
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
tests/cases/compiler/callsOnComplexSignatures.tsx(38,19): error TS7006: Parameter 'item' implicitly has an 'any' type.
2+
3+
4+
==== tests/cases/compiler/callsOnComplexSignatures.tsx (1 errors) ====
5+
/// <reference path="/.lib/react16.d.ts" />
6+
import React from "react";
7+
8+
// Simple calls from real usecases
9+
function test1() {
10+
type stringType1 = "foo" | "bar";
11+
type stringType2 = "baz" | "bar";
12+
13+
interface Temp1 {
14+
getValue(name: stringType1): number;
15+
}
16+
17+
interface Temp2 {
18+
getValue(name: stringType2): string;
19+
}
20+
21+
function test(t: Temp1 | Temp2) {
22+
const z = t.getValue("bar"); // Should be fine
23+
}
24+
}
25+
26+
function test2() {
27+
interface Messages {
28+
readonly foo: (options: { [key: string]: any, b: number }) => string;
29+
readonly bar: (options: { [key: string]: any, a: string }) => string;
30+
}
31+
32+
const messages: Messages = {
33+
foo: (options) => "Foo",
34+
bar: (options) => "Bar",
35+
};
36+
37+
const test1 = (type: "foo" | "bar") =>
38+
messages[type]({ a: "A", b: 0 });
39+
}
40+
41+
function test3(items: string[] | number[]) {
42+
items.forEach(item => console.log(item));
43+
~~~~
44+
!!! error TS7006: Parameter 'item' implicitly has an 'any' type.
45+
}
46+
47+
function test4(
48+
arg1: ((...objs: {x: number}[]) => number) | ((...objs: {y: number}[]) => number),
49+
arg2: ((a: {x: number}, b: object) => number) | ((a: object, b: {x: number}) => number),
50+
arg3: ((a: {x: number}, ...objs: {y: number}[]) => number) | ((...objs: {x: number}[]) => number),
51+
arg4: ((a?: {x: number}, b?: {x: number}) => number) | ((a?: {y: number}) => number),
52+
arg5: ((a?: {x: number}, ...b: {x: number}[]) => number) | ((a?: {y: number}) => number),
53+
arg6: ((a?: {x: number}, b?: {x: number}) => number) | ((...a: {y: number}[]) => number),
54+
) {
55+
arg1();
56+
arg1({x: 0, y: 0});
57+
arg1({x: 0, y: 0}, {x: 1, y: 1});
58+
59+
arg2({x: 0}, {x: 0});
60+
61+
arg3({x: 0});
62+
arg3({x: 0}, {x: 0, y: 0});
63+
arg3({x: 0}, {x: 0, y: 0}, {x: 0, y: 0});
64+
65+
arg4();
66+
arg4({x: 0, y: 0});
67+
arg4({x: 0, y: 0}, {x: 0});
68+
69+
arg5();
70+
arg5({x: 0, y: 0});
71+
arg5({x: 0, y: 0}, {x: 0});
72+
73+
arg6();
74+
arg6({x: 0, y: 0});
75+
arg6({x: 0, y: 0}, {x: 0, y: 0});
76+
arg6({x: 0, y: 0}, {x: 0, y: 0}, {y: 0});
77+
}
78+
79+
// JSX Tag names
80+
function test5() {
81+
// Pair of non-like intrinsics
82+
function render(url?: string): React.ReactNode {
83+
const Tag = url ? 'a' : 'button';
84+
return <Tag>test</Tag>;
85+
}
86+
87+
// Union of all intrinsics and components of `any`
88+
function App(props: { component:React.ReactType }) {
89+
const Comp: React.ReactType = props.component;
90+
return (<Comp />);
91+
}
92+
93+
// custom components with non-subset props
94+
function render2() {
95+
interface P1 {
96+
p?: boolean;
97+
c?: string;
98+
}
99+
interface P2 {
100+
p?: boolean;
101+
c?: any;
102+
d?: any;
103+
}
104+
105+
var C: React.ComponentType<P1> | React.ComponentType<P2> = null as any;
106+
107+
const a = <C p={true} />;
108+
}
109+
}
110+

0 commit comments

Comments
 (0)