Skip to content

Commit

Permalink
Contextually type complex signatures, rather than giving up
Browse files Browse the repository at this point in the history
  • Loading branch information
weswigham committed Sep 20, 2017
1 parent 7dec4ae commit 7167a3d
Show file tree
Hide file tree
Showing 8 changed files with 2,264 additions and 8 deletions.
101 changes: 99 additions & 2 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13392,6 +13392,97 @@ namespace ts {
getApparentTypeOfContextualType(node);
}

function combineSignatures(signatureList: Signature[]): Signature {
// Produce a synthetic signature whose arguments are a union of the parameters of the inferred signatures and whose return type is an intersection
let parameters: Symbol[];
let minimumParameterCount = Number.POSITIVE_INFINITY;
let maximumRealParameterCount = 0;
let restTypes: Type[];
let thisTypes: Type[];
let hasLiteralTypes = false;

// First, collect aggrgate statistics about all signatures to determine the characteristics of the resulting signature
for (const signature of signatureList) {
if (signature.minArgumentCount < minimumParameterCount) {
minimumParameterCount = signature.minArgumentCount;
}
if (signature.hasRestParameter) {
(restTypes || (restTypes = [])).push(getRestTypeOfSignature(signature));
}
if (signature.hasLiteralTypes) {
hasLiteralTypes = true;
}
const realParameterCount = length(signature.parameters) - (signature.hasRestParameter ? 1 : 0);
if (maximumRealParameterCount < realParameterCount) {
maximumRealParameterCount = realParameterCount;
}
if (signature.thisParameter) {
(thisTypes || (thisTypes = [])).push(getTypeOfSymbol(signature.thisParameter));
}
}

// Then, for every real parameter up to the maximum, combine those parameter types and names into a new symbol
for (let i = 0; i < maximumRealParameterCount; i++) {
const parameterNames: __String[] = [];
const parameterTypes: Type[] = [];
for (const signature of signatureList) {
let type: Type;
const index = signature.thisParameter ? i + 1 : i;
if (index < (signature.hasRestParameter ? signature.parameters.length - 1 : signature.parameters.length)) {
// If the argument is present, add it to the mixed argument
const param = signature.parameters[index];
if (!contains(parameterNames, param.escapedName)) {
parameterNames.push(param.escapedName);
}
type = getTypeOfSymbol(param);
}
else if (signature.hasRestParameter) {
// Otherwise, if there is a rest type for this signature, add that type
type = getRestTypeOfSignature(signature);
}
else {
// Otherwise, this argument may be `undefined` on some uses
type = undefinedType;
}
if (!contains(parameterTypes, type)) {
parameterTypes.push(type);
}
}
// We do this so the name is reasonable for users
const paramName = escapeLeadingUnderscores(map(parameterNames, unescapeLeadingUnderscores).join("or"));
const paramSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, paramName);
paramSymbol.type = getUnionType(parameterTypes);
(parameters || (parameters = [])).push(paramSymbol);
}

const hasRestParameter = !!(restTypes && restTypes.length);
if (hasRestParameter) {
const restSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, "rest" as __String);
restSymbol.type = getUnionType(restTypes);
restSymbol.isRestParameter = true;
(parameters || (parameters = [])).push(restSymbol);
}

let thisParameterSymbol: TransientSymbol;
if (thisTypes && thisTypes.length) {
thisParameterSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, "this" as __String);
thisParameterSymbol.type = getUnionType(thisTypes);
}

// TODO (weswigham): Merge type predicates?
return createSignature(
/*declaration*/ undefined,
map(flatMap(signatureList, s => s.typeParameters), cloneTypeParameter),
thisParameterSymbol,
parameters,
getIntersectionType(map(signatureList, getReturnTypeOfSignature)),
/*typePredicate*/ undefined,
minimumParameterCount,
hasRestParameter,
hasLiteralTypes
);
}

// Return the contextual signature for a given expression node. A contextual type provides a
// contextual signature if it has a single call signature and if that call signature is non-generic.
// If the contextual type is a union type, get the signature from each type possible and if they are
Expand All @@ -13408,6 +13499,7 @@ namespace ts {
}
let signatureList: Signature[];
const types = (<UnionType>type).types;
let mismatchedSignatures = false;
for (const current of types) {
const signature = getContextualCallSignature(current, node);
if (signature) {
Expand All @@ -13416,8 +13508,9 @@ namespace ts {
signatureList = [signature];
}
else if (!compareSignaturesIdentical(signatureList[0], signature, /*partialMatch*/ false, /*ignoreThisTypes*/ true, /*ignoreReturnTypes*/ true, compareTypesIdentical)) {
// Signatures aren't identical, do not use
return undefined;
// Signatures aren't identical, set flag to union parameter types, intersect return types
signatureList.push(signature);
mismatchedSignatures = true;
}
else {
// Use this signature for contextual union signature
Expand All @@ -13426,6 +13519,10 @@ namespace ts {
}
}

if (mismatchedSignatures) {
return combineSignatures(signatureList);
}

// Result is union of signatures collected (return type is union of return types of this signature set)
let result: Signature;
if (signatureList) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ var x3: IWithCallSignatures | IWithCallSignatures3 = a => /*here a should be any
>IWithCallSignatures : Symbol(IWithCallSignatures, Decl(contextualTypeWithUnionTypeCallSignatures.ts, 9, 1))
>IWithCallSignatures3 : Symbol(IWithCallSignatures3, Decl(contextualTypeWithUnionTypeCallSignatures.ts, 15, 1))
>a : Symbol(a, Decl(contextualTypeWithUnionTypeCallSignatures.ts, 32, 52))
>a.toString : Symbol(toString, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
>a : Symbol(a, Decl(contextualTypeWithUnionTypeCallSignatures.ts, 32, 52))
>toString : Symbol(toString, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))

// With call signature count mismatch
var x4: IWithCallSignatures | IWithCallSignatures4 = a => /*here a should be any*/ a.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,12 @@ var x3: IWithCallSignatures | IWithCallSignatures3 = a => /*here a should be any
>x3 : IWithCallSignatures | IWithCallSignatures3
>IWithCallSignatures : IWithCallSignatures
>IWithCallSignatures3 : IWithCallSignatures3
>a => /*here a should be any*/ a.toString() : (a: any) => any
>a : any
>a.toString() : any
>a.toString : any
>a : any
>toString : any
>a => /*here a should be any*/ a.toString() : (a: string | number) => string
>a : string | number
>a.toString() : string
>a.toString : ((radix?: number) => string) | (() => string)
>a : string | number
>toString : ((radix?: number) => string) | (() => string)

// With call signature count mismatch
var x4: IWithCallSignatures | IWithCallSignatures4 = a => /*here a should be any*/ a.toString();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
tests/cases/conformance/expressions/contextualTyping/functionExpressionContextualTyping1.ts(43,17): error TS2339: Property 'toLowerCase' does not exist on type 'string | number'.
Property 'toLowerCase' does not exist on type 'number'.
tests/cases/conformance/expressions/contextualTyping/functionExpressionContextualTyping1.ts(45,7): error TS2339: Property 'toExponential' does not exist on type 'string | number'.
Property 'toExponential' does not exist on type 'string'.
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[])'.
Type '(j: number | T, k: U) => (number | T | U)[]' is not assignable to type '(j: number, k: U) => number[]'.
Type '(number | T | U)[]' is not assignable to type 'number[]'.
Type 'number | T | U' is not assignable to type 'number'.
Type 'T' is not assignable to type 'number'.


==== tests/cases/conformance/expressions/contextualTyping/functionExpressionContextualTyping1.ts (3 errors) ====
// When a function expression with no type parameters and no parameter type annotations
// is contextually typed (section 4.19) by a type T and a contextual signature S can be extracted from T

enum E { red, blue }

// A contextual signature S is extracted from a function type T as follows:
// If T is a function type with exactly one call signature, and if that call signature is non- generic, S is that signature.

var a0: (n: number, s: string) => number = (num, str) => {
num.toExponential();
return 0;
}

class Class<T> {
foo() { }
}

var a1: (c: Class<Number>) => number = (a1) => {
a1.foo();
return 1;
}

// A contextual signature S is extracted from a function type T as follows:
// If T is a union type, let U be the set of element types in T that have call signatures.
// If each type in U has exactly one call signature and that call signature is non- generic,
// and if all of the signatures are identical ignoring return types,
// then S is a signature with the same parameters and a union of the return types.
var b1: ((s: string, w: boolean) => void) | ((s: string, w: boolean) => string);
b1 = (k, h) => { };
var b2: typeof a0 | ((n: number, s: string) => string);
b2 = (foo, bar) => { return foo + 1; }
b2 = (foo, bar) => { return "hello"; }
var b3: (name: string, num: number, boo: boolean) => void;
b3 = (name, number) => { };

var b4: (n: E) => string = (number = 1) => { return "hello"; };
var b5: (n: {}) => string = (number = "string") => { return "hello"; };

// A contextual signature S is extracted from a function type T as follows:
// Otherwise, no contextual signature can be extracted from T and S is undefined.
var b6: ((s: string, w: boolean) => void) | ((n: number) => number);
var b7: ((s: string, w: boolean) => void) | ((s: string, w: number) => string);
b6 = (k) => { k.toLowerCase() };
~~~~~~~~~~~
!!! error TS2339: Property 'toLowerCase' does not exist on type 'string | number'.
!!! error TS2339: Property 'toLowerCase' does not exist on type 'number'.
b6 = (i) => {
i.toExponential();
~~~~~~~~~~~~~
!!! error TS2339: Property 'toExponential' does not exist on type 'string | number'.
!!! error TS2339: Property 'toExponential' does not exist on type 'string'.
return i;
}; // Per spec, no contextual signature can be extracted in this case. (Otherwise clause)
b7 = (j, m) => { }; // Per spec, no contextual signature can be extracted in this case. (Otherwise clause)

class C<T, U> {
constructor() {
var k: ((j: T, k: U) => (T|U)[]) | ((j: number,k :U) => number[]) = (j, k) => {
~
!!! 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[])'.
!!! error TS2322: Type '(j: number | T, k: U) => (number | T | U)[]' is not assignable to type '(j: number, k: U) => number[]'.
!!! error TS2322: Type '(number | T | U)[]' is not assignable to type 'number[]'.
!!! error TS2322: Type 'number | T | U' is not assignable to type 'number'.
!!! error TS2322: Type 'T' is not assignable to type 'number'.
return [j, k];
} // Per spec, no contextual signature can be extracted in this case.
}
}
Loading

0 comments on commit 7167a3d

Please sign in to comment.