Skip to content

Commit c6adbf8

Browse files
committed
Contextually type complex signatures, rather than giving up
1 parent deefb01 commit c6adbf8

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
@@ -13297,6 +13297,97 @@ namespace ts {
1329713297
getApparentTypeOfContextualType(node);
1329813298
}
1329913299

13300+
function combineSignatures(signatureList: Signature[]): Signature {
13301+
// Produce a synthetic signature whose arguments are a union of the parameters of the inferred signatures and whose return type is an intersection
13302+
let parameters: Symbol[];
13303+
let minimumParameterCount = Number.POSITIVE_INFINITY;
13304+
let maximumRealParameterCount = 0;
13305+
let restTypes: Type[];
13306+
let thisTypes: Type[];
13307+
let hasLiteralTypes = false;
13308+
13309+
// First, collect aggrgate statistics about all signatures to determine the characteristics of the resulting signature
13310+
for (const signature of signatureList) {
13311+
if (signature.minArgumentCount < minimumParameterCount) {
13312+
minimumParameterCount = signature.minArgumentCount;
13313+
}
13314+
if (signature.hasRestParameter) {
13315+
(restTypes || (restTypes = [])).push(getRestTypeOfSignature(signature));
13316+
}
13317+
if (signature.hasLiteralTypes) {
13318+
hasLiteralTypes = true;
13319+
}
13320+
const realParameterCount = length(signature.parameters) - (signature.hasRestParameter ? 1 : 0);
13321+
if (maximumRealParameterCount < realParameterCount) {
13322+
maximumRealParameterCount = realParameterCount;
13323+
}
13324+
if (signature.thisParameter) {
13325+
(thisTypes || (thisTypes = [])).push(getTypeOfSymbol(signature.thisParameter));
13326+
}
13327+
}
13328+
13329+
// Then, for every real parameter up to the maximum, combine those parameter types and names into a new symbol
13330+
for (let i = 0; i < maximumRealParameterCount; i++) {
13331+
const parameterNames: __String[] = [];
13332+
const parameterTypes: Type[] = [];
13333+
for (const signature of signatureList) {
13334+
let type: Type;
13335+
const index = signature.thisParameter ? i + 1 : i;
13336+
if (index < (signature.hasRestParameter ? signature.parameters.length - 1 : signature.parameters.length)) {
13337+
// If the argument is present, add it to the mixed argument
13338+
const param = signature.parameters[index];
13339+
if (!contains(parameterNames, param.escapedName)) {
13340+
parameterNames.push(param.escapedName);
13341+
}
13342+
type = getTypeOfSymbol(param);
13343+
}
13344+
else if (signature.hasRestParameter) {
13345+
// Otherwise, if there is a rest type for this signature, add that type
13346+
type = getRestTypeOfSignature(signature);
13347+
}
13348+
else {
13349+
// Otherwise, this argument may be `undefined` on some uses
13350+
type = undefinedType;
13351+
}
13352+
if (!contains(parameterTypes, type)) {
13353+
parameterTypes.push(type);
13354+
}
13355+
}
13356+
// We do this so the name is reasonable for users
13357+
const paramName = escapeLeadingUnderscores(map(parameterNames, unescapeLeadingUnderscores).join("or"));
13358+
const paramSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, paramName);
13359+
paramSymbol.type = getUnionType(parameterTypes);
13360+
(parameters || (parameters = [])).push(paramSymbol);
13361+
}
13362+
13363+
const hasRestParameter = !!(restTypes && restTypes.length);
13364+
if (hasRestParameter) {
13365+
const restSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, "rest" as __String);
13366+
restSymbol.type = getUnionType(restTypes);
13367+
restSymbol.isRestParameter = true;
13368+
(parameters || (parameters = [])).push(restSymbol);
13369+
}
13370+
13371+
let thisParameterSymbol: TransientSymbol;
13372+
if (thisTypes && thisTypes.length) {
13373+
thisParameterSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, "this" as __String);
13374+
thisParameterSymbol.type = getUnionType(thisTypes);
13375+
}
13376+
13377+
// TODO (weswigham): Merge type predicates?
13378+
return createSignature(
13379+
/*declaration*/ undefined,
13380+
map(flatMap(signatureList, s => s.typeParameters), cloneTypeParameter),
13381+
thisParameterSymbol,
13382+
parameters,
13383+
getIntersectionType(map(signatureList, getReturnTypeOfSignature)),
13384+
/*typePredicate*/ undefined,
13385+
minimumParameterCount,
13386+
hasRestParameter,
13387+
hasLiteralTypes
13388+
);
13389+
}
13390+
1330013391
// Return the contextual signature for a given expression node. A contextual type provides a
1330113392
// contextual signature if it has a single call signature and if that call signature is non-generic.
1330213393
// If the contextual type is a union type, get the signature from each type possible and if they are
@@ -13313,6 +13404,7 @@ namespace ts {
1331313404
}
1331413405
let signatureList: Signature[];
1331513406
const types = (<UnionType>type).types;
13407+
let mismatchedSignatures = false;
1331613408
for (const current of types) {
1331713409
const signature = getContextualCallSignature(current, node);
1331813410
if (signature) {
@@ -13321,8 +13413,9 @@ namespace ts {
1332113413
signatureList = [signature];
1332213414
}
1332313415
else if (!compareSignaturesIdentical(signatureList[0], signature, /*partialMatch*/ false, /*ignoreThisTypes*/ true, /*ignoreReturnTypes*/ true, compareTypesIdentical)) {
13324-
// Signatures aren't identical, do not use
13325-
return undefined;
13416+
// Signatures aren't identical, set flag to union parameter types, intersect return types
13417+
signatureList.push(signature);
13418+
mismatchedSignatures = true;
1332613419
}
1332713420
else {
1332813421
// Use this signature for contextual union signature
@@ -13331,6 +13424,10 @@ namespace ts {
1333113424
}
1333213425
}
1333313426

13427+
if (mismatchedSignatures) {
13428+
return combineSignatures(signatureList);
13429+
}
13430+
1333413431
// Result is union of signatures collected (return type is union of return types of this signature set)
1333513432
let result: Signature;
1333613433
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)