Skip to content
107 changes: 107 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6799,7 +6799,12 @@ namespace ts {
// type is the union of the constituent return types.
function getUnionSignatures(signatureLists: ReadonlyArray<ReadonlyArray<Signature>>): Signature[] {
let result: Signature[] | undefined;
let indexWithLengthOverOne: number | undefined;
for (let i = 0; i < signatureLists.length; i++) {
if (signatureLists[i].length === 0) return emptyArray;
if (signatureLists[i].length > 1) {
indexWithLengthOverOne = indexWithLengthOverOne === undefined ? i : -1; // -1 is a signal there are multiple overload sets
}
for (const signature of signatureLists[i]) {
// Only process signatures with parameter lists that aren't already in the result list
if (!result || !findMatchingSignature(result, signature, /*partialMatch*/ false, /*ignoreThisTypes*/ true, /*ignoreReturnTypes*/ true)) {
Expand All @@ -6823,9 +6828,91 @@ namespace ts {
}
}
}
if (!length(result) && indexWithLengthOverOne !== -1) {
// No sufficiently similar signature existed to subsume all the other signatures in the union - time to see if we can make a single
// signature that handles all over them. We only do this when there are overloads in only one constituent.
// (Overloads are conditional in nature and having overloads in multiple constituents would necessitate making a power set of
// signatures from the type, whose ordering would be non-obvious)
const masterList = signatureLists[indexWithLengthOverOne !== undefined ? indexWithLengthOverOne : 0];
let results: Signature[] | undefined = masterList.slice();
for (const signatures of signatureLists) {
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));
if (!results) {
break;
}
}
}
result = results;
}
return result || emptyArray;
}

function combineUnionThisParam(left: Symbol | undefined, right: Symbol | 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 union the `this` types just like the overlapping-union-signature check does
const thisType = getUnionType([getTypeOfSymbol(left), getTypeOfSymbol(right)], UnionReduction.Subtype);
return createSymbolWithType(left, thisType);
}

function combineUnionParameters(left: Signature, right: Signature) {
const longest = getParameterCount(left) >= getParameterCount(right) ? left : right;
const shorter = longest === left ? right : left;
const longestCount = getParameterCount(longest);
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++) {
const longestParamType = tryGetTypeAtPosition(longest, i)!;
const shorterParamType = tryGetTypeAtPosition(shorter, i) || unknownType;
const unionParamType = getIntersectionType([longestParamType, shorterParamType]);
const isRestParam = eitherHasEffectiveRest && !needsExtraRestElement && i === (longestCount - 1);
const isOptional = i >= getMinArgumentCount(longest) && i >= getMinArgumentCount(shorter);
const leftName = getParameterNameAtPosition(left, i);
const rightName = getParameterNameAtPosition(right, i);
const paramSymbol = createSymbol(
SymbolFlags.FunctionScopedVariable | (isOptional && !isRestParam ? SymbolFlags.Optional : 0),
leftName === rightName ? leftName : `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));
params[longestCount] = restParamSymbol;
}
return params;
}

function combineSignaturesOfUnionMembers(left: Signature, right: Signature): Signature {
const declaration = left.declaration;
const params = combineUnionParameters(left, right);
const thisParam = combineUnionThisParam(left.thisParameter, right.thisParameter);
const minArgCount = Math.max(left.minArgumentCount, right.minArgumentCount);
const hasRestParam = left.hasRestParameter || right.hasRestParameter;
const hasLiteralTypes = left.hasLiteralTypes || right.hasLiteralTypes;
const result = createSignature(
declaration,
left.typeParameters || right.typeParameters,
thisParam,
params,
/*resolvedReturnType*/ undefined,
/*resolvedTypePredicate*/ undefined,
minArgCount,
hasRestParam,
hasLiteralTypes
);
result.unionSignatures = concatenate(left.unionSignatures || [left], [right]);
return result;
}

function getUnionIndexInfo(types: ReadonlyArray<Type>, kind: IndexKind): IndexInfo | undefined {
const indexTypes: Type[] = [];
let isAnyReadonly = false;
Expand Down Expand Up @@ -17566,6 +17653,26 @@ namespace ts {
}

function getJsxPropsTypeForSignatureFromMember(sig: Signature, forcedLookupLocation: __String) {
if (sig.unionSignatures) {
// 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) {
const instance = getReturnTypeOfSignature(signature);
if (isTypeAny(instance)) {
return instance;
}
const propType = getTypeOfPropertyOfType(instance, forcedLookupLocation);
if (!propType) {
return;
}
results.push(propType);
}
return getIntersectionType(results);
}
const instanceType = getReturnTypeOfSignature(sig);
return isTypeAny(instanceType) ? instanceType : getTypeOfPropertyOfType(instanceType, forcedLookupLocation);
}
Expand Down
110 changes: 110 additions & 0 deletions tests/baselines/reference/callsOnComplexSignatures.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
tests/cases/compiler/callsOnComplexSignatures.tsx(38,19): error TS7006: Parameter 'item' implicitly has an 'any' type.


==== tests/cases/compiler/callsOnComplexSignatures.tsx (1 errors) ====
/// <reference path="/.lib/react16.d.ts" />
import React from "react";

// Simple calls from real usecases
function test1() {
type stringType1 = "foo" | "bar";
type stringType2 = "baz" | "bar";

interface Temp1 {
getValue(name: stringType1): number;
}

interface Temp2 {
getValue(name: stringType2): string;
}

function test(t: Temp1 | Temp2) {
const z = t.getValue("bar"); // Should be fine
}
}

function test2() {
interface Messages {
readonly foo: (options: { [key: string]: any, b: number }) => string;
readonly bar: (options: { [key: string]: any, a: string }) => string;
}

const messages: Messages = {
foo: (options) => "Foo",
bar: (options) => "Bar",
};

const test1 = (type: "foo" | "bar") =>
messages[type]({ a: "A", b: 0 });
}

function test3(items: string[] | number[]) {
items.forEach(item => console.log(item));
~~~~
!!! error TS7006: Parameter 'item' implicitly has an 'any' type.
}

function test4(
arg1: ((...objs: {x: number}[]) => number) | ((...objs: {y: number}[]) => number),
arg2: ((a: {x: number}, b: object) => number) | ((a: object, b: {x: number}) => number),
arg3: ((a: {x: number}, ...objs: {y: number}[]) => number) | ((...objs: {x: number}[]) => number),
arg4: ((a?: {x: number}, b?: {x: number}) => number) | ((a?: {y: number}) => number),
arg5: ((a?: {x: number}, ...b: {x: number}[]) => number) | ((a?: {y: number}) => number),
arg6: ((a?: {x: number}, b?: {x: number}) => number) | ((...a: {y: number}[]) => number),
) {
arg1();
arg1({x: 0, y: 0});
arg1({x: 0, y: 0}, {x: 1, y: 1});

arg2({x: 0}, {x: 0});

arg3({x: 0});
arg3({x: 0}, {x: 0, y: 0});
arg3({x: 0}, {x: 0, y: 0}, {x: 0, y: 0});

arg4();
arg4({x: 0, y: 0});
arg4({x: 0, y: 0}, {x: 0});

arg5();
arg5({x: 0, y: 0});
arg5({x: 0, y: 0}, {x: 0});

arg6();
arg6({x: 0, y: 0});
arg6({x: 0, y: 0}, {x: 0, y: 0});
arg6({x: 0, y: 0}, {x: 0, y: 0}, {y: 0});
}

// JSX Tag names
function test5() {
// Pair of non-like intrinsics
function render(url?: string): React.ReactNode {
const Tag = url ? 'a' : 'button';
return <Tag>test</Tag>;
}

// Union of all intrinsics and components of `any`
function App(props: { component:React.ReactType }) {
const Comp: React.ReactType = props.component;
return (<Comp />);
}

// custom components with non-subset props
function render2() {
interface P1 {
p?: boolean;
c?: string;
}
interface P2 {
p?: boolean;
c?: any;
d?: any;
}

var C: React.ComponentType<P1> | React.ComponentType<P2> = null as any;

const a = <C p={true} />;
}
}

Loading