Skip to content

Commit 383ac75

Browse files
committed
Contextually type complex signatures, rather than giving up
1 parent 93abebc commit 383ac75

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
@@ -13264,6 +13264,97 @@ namespace ts {
1326413264
getApparentTypeOfContextualType(node);
1326513265
}
1326613266

13267+
function combineSignatures(signatureList: Signature[]): Signature {
13268+
// Produce a synthetic signature whose arguments are a union of the parameters of the inferred signatures and whose return type is an intersection
13269+
let parameters: Symbol[];
13270+
let minimumParameterCount = Number.POSITIVE_INFINITY;
13271+
let maximumRealParameterCount = 0;
13272+
let restTypes: Type[];
13273+
let thisTypes: Type[];
13274+
let hasLiteralTypes = false;
13275+
13276+
// First, collect aggrgate statistics about all signatures to determine the characteristics of the resulting signature
13277+
for (const signature of signatureList) {
13278+
if (signature.minArgumentCount < minimumParameterCount) {
13279+
minimumParameterCount = signature.minArgumentCount;
13280+
}
13281+
if (signature.hasRestParameter) {
13282+
(restTypes || (restTypes = [])).push(getRestTypeOfSignature(signature));
13283+
}
13284+
if (signature.hasLiteralTypes) {
13285+
hasLiteralTypes = true;
13286+
}
13287+
const realParameterCount = length(signature.parameters) - (signature.hasRestParameter ? 1 : 0);
13288+
if (maximumRealParameterCount < realParameterCount) {
13289+
maximumRealParameterCount = realParameterCount;
13290+
}
13291+
if (signature.thisParameter) {
13292+
(thisTypes || (thisTypes = [])).push(getTypeOfSymbol(signature.thisParameter));
13293+
}
13294+
}
13295+
13296+
// Then, for every real parameter up to the maximum, combine those parameter types and names into a new symbol
13297+
for (let i = 0; i < maximumRealParameterCount; i++) {
13298+
const parameterNames: __String[] = [];
13299+
const parameterTypes: Type[] = [];
13300+
for (const signature of signatureList) {
13301+
let type: Type;
13302+
const index = signature.thisParameter ? i + 1 : i;
13303+
if (index < (signature.hasRestParameter ? signature.parameters.length - 1 : signature.parameters.length)) {
13304+
// If the argument is present, add it to the mixed argument
13305+
const param = signature.parameters[index];
13306+
if (!contains(parameterNames, param.escapedName)) {
13307+
parameterNames.push(param.escapedName);
13308+
}
13309+
type = getTypeOfSymbol(param);
13310+
}
13311+
else if (signature.hasRestParameter) {
13312+
// Otherwise, if there is a rest type for this signature, add that type
13313+
type = getRestTypeOfSignature(signature);
13314+
}
13315+
else {
13316+
// Otherwise, this argument may be `undefined` on some uses
13317+
type = undefinedType;
13318+
}
13319+
if (!contains(parameterTypes, type)) {
13320+
parameterTypes.push(type);
13321+
}
13322+
}
13323+
// We do this so the name is reasonable for users
13324+
const paramName = escapeLeadingUnderscores(map(parameterNames, unescapeLeadingUnderscores).join("or"));
13325+
const paramSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, paramName);
13326+
paramSymbol.type = getUnionType(parameterTypes);
13327+
(parameters || (parameters = [])).push(paramSymbol);
13328+
}
13329+
13330+
const hasRestParameter = !!(restTypes && restTypes.length);
13331+
if (hasRestParameter) {
13332+
const restSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, "rest" as __String);
13333+
restSymbol.type = getUnionType(restTypes);
13334+
restSymbol.isRestParameter = true;
13335+
(parameters || (parameters = [])).push(restSymbol);
13336+
}
13337+
13338+
let thisParameterSymbol: TransientSymbol;
13339+
if (thisTypes && thisTypes.length) {
13340+
thisParameterSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, "this" as __String);
13341+
thisParameterSymbol.type = getUnionType(thisTypes);
13342+
}
13343+
13344+
// TODO (weswigham): Merge type predicates?
13345+
return createSignature(
13346+
/*declaration*/ undefined,
13347+
map(flatMap(signatureList, s => s.typeParameters), cloneTypeParameter),
13348+
thisParameterSymbol,
13349+
parameters,
13350+
getIntersectionType(map(signatureList, getReturnTypeOfSignature)),
13351+
/*typePredicate*/ undefined,
13352+
minimumParameterCount,
13353+
hasRestParameter,
13354+
hasLiteralTypes
13355+
);
13356+
}
13357+
1326713358
// Return the contextual signature for a given expression node. A contextual type provides a
1326813359
// contextual signature if it has a single call signature and if that call signature is non-generic.
1326913360
// If the contextual type is a union type, get the signature from each type possible and if they are
@@ -13280,6 +13371,7 @@ namespace ts {
1328013371
}
1328113372
let signatureList: Signature[];
1328213373
const types = (<UnionType>type).types;
13374+
let mismatchedSignatures = false;
1328313375
for (const current of types) {
1328413376
const signature = getContextualCallSignature(current, node);
1328513377
if (signature) {
@@ -13288,8 +13380,9 @@ namespace ts {
1328813380
signatureList = [signature];
1328913381
}
1329013382
else if (!compareSignaturesIdentical(signatureList[0], signature, /*partialMatch*/ false, /*ignoreThisTypes*/ true, /*ignoreReturnTypes*/ true, compareTypesIdentical)) {
13291-
// Signatures aren't identical, do not use
13292-
return undefined;
13383+
// Signatures aren't identical, set flag to union parameter types, intersect return types
13384+
signatureList.push(signature);
13385+
mismatchedSignatures = true;
1329313386
}
1329413387
else {
1329513388
// Use this signature for contextual union signature
@@ -13298,6 +13391,10 @@ namespace ts {
1329813391
}
1329913392
}
1330013393

13394+
if (mismatchedSignatures) {
13395+
return combineSignatures(signatureList);
13396+
}
13397+
1330113398
// Result is union of signatures collected (return type is union of return types of this signature set)
1330213399
let result: Signature;
1330313400
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)