Skip to content

Commit 7167a3d

Browse files
committed
Contextually type complex signatures, rather than giving up
1 parent 7dec4ae commit 7167a3d

8 files changed

+2264
-8
lines changed

src/compiler/checker.ts

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13392,6 +13392,97 @@ namespace ts {
1339213392
getApparentTypeOfContextualType(node);
1339313393
}
1339413394

13395+
function combineSignatures(signatureList: Signature[]): Signature {
13396+
// Produce a synthetic signature whose arguments are a union of the parameters of the inferred signatures and whose return type is an intersection
13397+
let parameters: Symbol[];
13398+
let minimumParameterCount = Number.POSITIVE_INFINITY;
13399+
let maximumRealParameterCount = 0;
13400+
let restTypes: Type[];
13401+
let thisTypes: Type[];
13402+
let hasLiteralTypes = false;
13403+
13404+
// First, collect aggrgate statistics about all signatures to determine the characteristics of the resulting signature
13405+
for (const signature of signatureList) {
13406+
if (signature.minArgumentCount < minimumParameterCount) {
13407+
minimumParameterCount = signature.minArgumentCount;
13408+
}
13409+
if (signature.hasRestParameter) {
13410+
(restTypes || (restTypes = [])).push(getRestTypeOfSignature(signature));
13411+
}
13412+
if (signature.hasLiteralTypes) {
13413+
hasLiteralTypes = true;
13414+
}
13415+
const realParameterCount = length(signature.parameters) - (signature.hasRestParameter ? 1 : 0);
13416+
if (maximumRealParameterCount < realParameterCount) {
13417+
maximumRealParameterCount = realParameterCount;
13418+
}
13419+
if (signature.thisParameter) {
13420+
(thisTypes || (thisTypes = [])).push(getTypeOfSymbol(signature.thisParameter));
13421+
}
13422+
}
13423+
13424+
// Then, for every real parameter up to the maximum, combine those parameter types and names into a new symbol
13425+
for (let i = 0; i < maximumRealParameterCount; i++) {
13426+
const parameterNames: __String[] = [];
13427+
const parameterTypes: Type[] = [];
13428+
for (const signature of signatureList) {
13429+
let type: Type;
13430+
const index = signature.thisParameter ? i + 1 : i;
13431+
if (index < (signature.hasRestParameter ? signature.parameters.length - 1 : signature.parameters.length)) {
13432+
// If the argument is present, add it to the mixed argument
13433+
const param = signature.parameters[index];
13434+
if (!contains(parameterNames, param.escapedName)) {
13435+
parameterNames.push(param.escapedName);
13436+
}
13437+
type = getTypeOfSymbol(param);
13438+
}
13439+
else if (signature.hasRestParameter) {
13440+
// Otherwise, if there is a rest type for this signature, add that type
13441+
type = getRestTypeOfSignature(signature);
13442+
}
13443+
else {
13444+
// Otherwise, this argument may be `undefined` on some uses
13445+
type = undefinedType;
13446+
}
13447+
if (!contains(parameterTypes, type)) {
13448+
parameterTypes.push(type);
13449+
}
13450+
}
13451+
// We do this so the name is reasonable for users
13452+
const paramName = escapeLeadingUnderscores(map(parameterNames, unescapeLeadingUnderscores).join("or"));
13453+
const paramSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, paramName);
13454+
paramSymbol.type = getUnionType(parameterTypes);
13455+
(parameters || (parameters = [])).push(paramSymbol);
13456+
}
13457+
13458+
const hasRestParameter = !!(restTypes && restTypes.length);
13459+
if (hasRestParameter) {
13460+
const restSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, "rest" as __String);
13461+
restSymbol.type = getUnionType(restTypes);
13462+
restSymbol.isRestParameter = true;
13463+
(parameters || (parameters = [])).push(restSymbol);
13464+
}
13465+
13466+
let thisParameterSymbol: TransientSymbol;
13467+
if (thisTypes && thisTypes.length) {
13468+
thisParameterSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, "this" as __String);
13469+
thisParameterSymbol.type = getUnionType(thisTypes);
13470+
}
13471+
13472+
// TODO (weswigham): Merge type predicates?
13473+
return createSignature(
13474+
/*declaration*/ undefined,
13475+
map(flatMap(signatureList, s => s.typeParameters), cloneTypeParameter),
13476+
thisParameterSymbol,
13477+
parameters,
13478+
getIntersectionType(map(signatureList, getReturnTypeOfSignature)),
13479+
/*typePredicate*/ undefined,
13480+
minimumParameterCount,
13481+
hasRestParameter,
13482+
hasLiteralTypes
13483+
);
13484+
}
13485+
1339513486
// Return the contextual signature for a given expression node. A contextual type provides a
1339613487
// contextual signature if it has a single call signature and if that call signature is non-generic.
1339713488
// If the contextual type is a union type, get the signature from each type possible and if they are
@@ -13408,6 +13499,7 @@ namespace ts {
1340813499
}
1340913500
let signatureList: Signature[];
1341013501
const types = (<UnionType>type).types;
13502+
let mismatchedSignatures = false;
1341113503
for (const current of types) {
1341213504
const signature = getContextualCallSignature(current, node);
1341313505
if (signature) {
@@ -13416,8 +13508,9 @@ namespace ts {
1341613508
signatureList = [signature];
1341713509
}
1341813510
else if (!compareSignaturesIdentical(signatureList[0], signature, /*partialMatch*/ false, /*ignoreThisTypes*/ true, /*ignoreReturnTypes*/ true, compareTypesIdentical)) {
13419-
// Signatures aren't identical, do not use
13420-
return undefined;
13511+
// Signatures aren't identical, set flag to union parameter types, intersect return types
13512+
signatureList.push(signature);
13513+
mismatchedSignatures = true;
1342113514
}
1342213515
else {
1342313516
// Use this signature for contextual union signature
@@ -13426,6 +13519,10 @@ namespace ts {
1342613519
}
1342713520
}
1342813521

13522+
if (mismatchedSignatures) {
13523+
return combineSignatures(signatureList);
13524+
}
13525+
1342913526
// Result is union of signatures collected (return type is union of return types of this signature set)
1343013527
let result: Signature;
1343113528
if (signatureList) {

tests/baselines/reference/contextualTypeWithUnionTypeCallSignatures.symbols

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ var x3: IWithCallSignatures | IWithCallSignatures3 = a => /*here a should be any
7474
>IWithCallSignatures : Symbol(IWithCallSignatures, Decl(contextualTypeWithUnionTypeCallSignatures.ts, 9, 1))
7575
>IWithCallSignatures3 : Symbol(IWithCallSignatures3, Decl(contextualTypeWithUnionTypeCallSignatures.ts, 15, 1))
7676
>a : Symbol(a, Decl(contextualTypeWithUnionTypeCallSignatures.ts, 32, 52))
77+
>a.toString : Symbol(toString, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
7778
>a : Symbol(a, Decl(contextualTypeWithUnionTypeCallSignatures.ts, 32, 52))
79+
>toString : Symbol(toString, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
7880

7981
// With call signature count mismatch
8082
var x4: IWithCallSignatures | IWithCallSignatures4 = a => /*here a should be any*/ a.toString();

tests/baselines/reference/contextualTypeWithUnionTypeCallSignatures.types

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,12 @@ var x3: IWithCallSignatures | IWithCallSignatures3 = a => /*here a should be any
7878
>x3 : IWithCallSignatures | IWithCallSignatures3
7979
>IWithCallSignatures : IWithCallSignatures
8080
>IWithCallSignatures3 : IWithCallSignatures3
81-
>a => /*here a should be any*/ a.toString() : (a: any) => any
82-
>a : any
83-
>a.toString() : any
84-
>a.toString : any
85-
>a : any
86-
>toString : any
81+
>a => /*here a should be any*/ a.toString() : (a: string | number) => string
82+
>a : string | number
83+
>a.toString() : string
84+
>a.toString : ((radix?: number) => string) | (() => string)
85+
>a : string | number
86+
>toString : ((radix?: number) => string) | (() => string)
8787

8888
// With call signature count mismatch
8989
var x4: IWithCallSignatures | IWithCallSignatures4 = a => /*here a should be any*/ a.toString();
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
tests/cases/conformance/expressions/contextualTyping/functionExpressionContextualTyping1.ts(43,17): error TS2339: Property 'toLowerCase' does not exist on type 'string | number'.
2+
Property 'toLowerCase' does not exist on type 'number'.
3+
tests/cases/conformance/expressions/contextualTyping/functionExpressionContextualTyping1.ts(45,7): error TS2339: Property 'toExponential' does not exist on type 'string | number'.
4+
Property 'toExponential' does not exist on type 'string'.
5+
tests/cases/conformance/expressions/contextualTyping/functionExpressionContextualTyping1.ts(52,13): error TS2322: Type '(j: number | T, k: U) => (number | T | U)[]' is not assignable to type '((j: T, k: U) => (T | U)[]) | ((j: number, k: U) => number[])'.
6+
Type '(j: number | T, k: U) => (number | T | U)[]' is not assignable to type '(j: number, k: U) => number[]'.
7+
Type '(number | T | U)[]' is not assignable to type 'number[]'.
8+
Type 'number | T | U' is not assignable to type 'number'.
9+
Type 'T' is not assignable to type 'number'.
10+
11+
12+
==== tests/cases/conformance/expressions/contextualTyping/functionExpressionContextualTyping1.ts (3 errors) ====
13+
// When a function expression with no type parameters and no parameter type annotations
14+
// is contextually typed (section 4.19) by a type T and a contextual signature S can be extracted from T
15+
16+
enum E { red, blue }
17+
18+
// A contextual signature S is extracted from a function type T as follows:
19+
// If T is a function type with exactly one call signature, and if that call signature is non- generic, S is that signature.
20+
21+
var a0: (n: number, s: string) => number = (num, str) => {
22+
num.toExponential();
23+
return 0;
24+
}
25+
26+
class Class<T> {
27+
foo() { }
28+
}
29+
30+
var a1: (c: Class<Number>) => number = (a1) => {
31+
a1.foo();
32+
return 1;
33+
}
34+
35+
// A contextual signature S is extracted from a function type T as follows:
36+
// If T is a union type, let U be the set of element types in T that have call signatures.
37+
// If each type in U has exactly one call signature and that call signature is non- generic,
38+
// and if all of the signatures are identical ignoring return types,
39+
// then S is a signature with the same parameters and a union of the return types.
40+
var b1: ((s: string, w: boolean) => void) | ((s: string, w: boolean) => string);
41+
b1 = (k, h) => { };
42+
var b2: typeof a0 | ((n: number, s: string) => string);
43+
b2 = (foo, bar) => { return foo + 1; }
44+
b2 = (foo, bar) => { return "hello"; }
45+
var b3: (name: string, num: number, boo: boolean) => void;
46+
b3 = (name, number) => { };
47+
48+
var b4: (n: E) => string = (number = 1) => { return "hello"; };
49+
var b5: (n: {}) => string = (number = "string") => { return "hello"; };
50+
51+
// A contextual signature S is extracted from a function type T as follows:
52+
// Otherwise, no contextual signature can be extracted from T and S is undefined.
53+
var b6: ((s: string, w: boolean) => void) | ((n: number) => number);
54+
var b7: ((s: string, w: boolean) => void) | ((s: string, w: number) => string);
55+
b6 = (k) => { k.toLowerCase() };
56+
~~~~~~~~~~~
57+
!!! error TS2339: Property 'toLowerCase' does not exist on type 'string | number'.
58+
!!! error TS2339: Property 'toLowerCase' does not exist on type 'number'.
59+
b6 = (i) => {
60+
i.toExponential();
61+
~~~~~~~~~~~~~
62+
!!! error TS2339: Property 'toExponential' does not exist on type 'string | number'.
63+
!!! error TS2339: Property 'toExponential' does not exist on type 'string'.
64+
return i;
65+
}; // Per spec, no contextual signature can be extracted in this case. (Otherwise clause)
66+
b7 = (j, m) => { }; // Per spec, no contextual signature can be extracted in this case. (Otherwise clause)
67+
68+
class C<T, U> {
69+
constructor() {
70+
var k: ((j: T, k: U) => (T|U)[]) | ((j: number,k :U) => number[]) = (j, k) => {
71+
~
72+
!!! error TS2322: Type '(j: number | T, k: U) => (number | T | U)[]' is not assignable to type '((j: T, k: U) => (T | U)[]) | ((j: number, k: U) => number[])'.
73+
!!! error TS2322: Type '(j: number | T, k: U) => (number | T | U)[]' is not assignable to type '(j: number, k: U) => number[]'.
74+
!!! error TS2322: Type '(number | T | U)[]' is not assignable to type 'number[]'.
75+
!!! error TS2322: Type 'number | T | U' is not assignable to type 'number'.
76+
!!! error TS2322: Type 'T' is not assignable to type 'number'.
77+
return [j, k];
78+
} // Per spec, no contextual signature can be extracted in this case.
79+
}
80+
}

0 commit comments

Comments
 (0)