Skip to content

Allow calls on unions of dissimilar signatures #29011

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 13 commits into from
Dec 20, 2018
196 changes: 187 additions & 9 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6729,7 +6729,17 @@ 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) {
if (indexWithLengthOverOne === undefined) {
indexWithLengthOverOne = i;
}
else {
indexWithLengthOverOne = -1; // 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 @@ -6753,9 +6763,132 @@ 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) continue;
const signature = signatures[0];
Debug.assert(!!signature, "getUnionSignatures bails early on empty signature lists and should not have empty lists on second pass");
results = mergeAsUnionSignature(results, signature);
if (!results) {
break;
}
}
result = results;
}
return result || emptyArray;
}

function mergeAsUnionSignature(masterList: Signature[], addition: Signature): Signature[] | undefined {
if (addition.typeParameters && some(masterList, s => !!s.typeParameters)) {
return; // Can't currently unify type parameters
}
return map(masterList, sig => combineSignaturesOfUnionMembers(sig, addition));
}

function combineUnionThisParam(left: Symbol | undefined, right: Symbol | undefined): Symbol | undefined {
if (!left && !right) {
return;
}
if (!left) {
return right;
}
if (!right) {
return left;
}
// 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 combinePartameterNames(index: number, left: __String | undefined, right: __String | undefined): __String {
if (!left && !right) {
return `arg${index}` as __String;
}
if (!left) {
return right!;
}
if (!right || left === right) {
return left;
}
const names = createMap<true>();
// We join names in an upperCamelCase fashion.
const seperator = "And";
const leftNames = unescapeLeadingUnderscores(left).split(seperator);
const rightNames = unescapeLeadingUnderscores(right).split(seperator);
let first = true;
for (const list of [leftNames, rightNames]) {
for (const n of list) {
names.set(first ? (first = false, n) : (n.slice(0, 1).toUpperCase() + n.slice(1)), true);
}
}
if (names.size > 5) {
// Combining >5 names just looks bad
return `arg${index}` as __String;
}
return escapeLeadingUnderscores(arrayFrom(names.keys()).join(seperator));
}

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));
let i = 0;
for (; i < longestCount; i++) {
const longestParamType = tryGetTypeAtPosition(longest, i)!;
const shorterParamType = tryGetTypeAtPosition(shorter, i) || unknownType;
let unionParamType = getIntersectionType([longestParamType, shorterParamType]);
const isRestParam = eitherHasEffectiveRest && !needsExtraRestElement && i === (longestCount - 1);
if (isRestParam) {
unionParamType = createArrayType(unionParamType);
}
const isOptional = i > getMinArgumentCount(longest);
const leftName = tryGetNameAtPosition(left, i);
const rightName = tryGetNameAtPosition(right, i);
const paramSymbol = createSymbol(SymbolFlags.FunctionScopedVariable | (isOptional && !isRestParam ? SymbolFlags.Optional : 0), combinePartameterNames(i, leftName, rightName));
paramSymbol.type = unionParamType;
params[i] = paramSymbol;
}
if (needsExtraRestElement) {
const restParamSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, "args" as __String);
restParamSymbol.type = createArrayType(getTypeAtPosition(shorter, i));
params[i] = 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 @@ -17533,6 +17666,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 Expand Up @@ -21078,24 +21231,49 @@ namespace ts {
return tryGetTypeAtPosition(signature, pos) || anyType;
}

function tryGetTypeAtPosition(signature: Signature, pos: number): Type | undefined {
function tryGetAtXPositionWorker<T>(
signature: Signature,
pos: number,
symbolGetter: (symbol: Symbol) => T,
restTypeGetter: (type: Type, pos: number, paramCount: number) => T): T | undefined {
const paramCount = signature.parameters.length - (signature.hasRestParameter ? 1 : 0);
if (pos < paramCount) {
return getTypeOfParameter(signature.parameters[pos]);
return symbolGetter(signature.parameters[pos]);
}
if (signature.hasRestParameter) {
const restType = getTypeOfSymbol(signature.parameters[paramCount]);
if (isTupleType(restType)) {
if (pos - paramCount < getLengthOfTupleType(restType)) {
return restType.typeArguments![pos - paramCount];
}
return getRestTypeOfTupleType(restType);
}
return getIndexTypeOfType(restType, IndexKind.Number);
return restTypeGetter(restType, pos, paramCount);
}
return undefined;
}

function getTupleTypeForArgumentAtPos(restType: Type, pos: number, paramCount: number) {
if (isTupleType(restType)) {
if (pos - paramCount < getLengthOfTupleType(restType)) {
return restType.typeArguments![pos - paramCount];
}
return getRestTypeOfTupleType(restType);
}
return getIndexTypeOfType(restType, IndexKind.Number);
}

function tryGetTypeAtPosition(signature: Signature, pos: number): Type | undefined {
return tryGetAtXPositionWorker(signature, pos, getTypeOfParameter, getTupleTypeForArgumentAtPos);
}

function getTupleParameterNameForArgumentAtPos(restType: Type, pos: number, paramCount: number): __String {
if (isTupleType(restType)) {
if (pos - paramCount < getLengthOfTupleType(restType) && restType.target.associatedNames) {
return restType.target.associatedNames[pos - paramCount];
}
}
return `arg${pos}` as __String;
}

function tryGetNameAtPosition(signature: Signature, pos: number): __String | undefined {
return tryGetAtXPositionWorker(signature, pos, s => s.escapedName, getTupleParameterNameForArgumentAtPos);
}

function getRestTypeAtPosition(source: Signature, pos: number): Type {
const paramCount = getParameterCount(source);
const restType = getEffectiveRestType(source);
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