Skip to content
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

Allow calls on unions of dissimilar signatures #29011

Merged
merged 13 commits into from
Dec 20, 2018
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++) {
weswigham marked this conversation as resolved.
Show resolved Hide resolved
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
weswigham marked this conversation as resolved.
Show resolved Hide resolved
}
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) {
weswigham marked this conversation as resolved.
Show resolved Hide resolved
// 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;
weswigham marked this conversation as resolved.
Show resolved Hide resolved
}

weswigham marked this conversation as resolved.
Show resolved Hide resolved
function combineUnionThisParam(left: Symbol | undefined, right: Symbol | undefined): Symbol | undefined {
if (!left || !right) {
return left || right;
weswigham marked this conversation as resolved.
Show resolved Hide resolved
}
// 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
weswigham marked this conversation as resolved.
Show resolved Hide resolved
const thisType = getUnionType([getTypeOfSymbol(left), getTypeOfSymbol(right)], UnionReduction.Subtype);
return createSymbolWithType(left, thisType);
}

function combineUnionParameters(left: Signature, right: Signature) {
weswigham marked this conversation as resolved.
Show resolved Hide resolved
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);
weswigham marked this conversation as resolved.
Show resolved Hide resolved
}
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